连接:http://my.oschina.net/sunchp/blog/369707

1.JVM内存模型

JVM运行时内存=共享内存区+线程内存区

1).共享内存区

共享内存区=持久带+堆

持久带=方法区+其他

堆=Old Space+Young Space

Young Space=Eden+S0+S1

(1)持久带

JVM用持久带(Permanent Space)实现方法区,主要存放所有已加载的类信息,方法信息,常量池等等。

可通过-XX:PermSize和-XX:MaxPermSize来指定持久带初始化值和最大值。

Permanent Space并不等同于方法区,只不过是Hotspot JVM用Permanent Space来实现方法区而已,有些虚拟机没有Permanent Space而用其他机制来实现方法区。

(2)堆

堆,主要用来存放类的对象实例信息。

堆分为Old Space(又名,Tenured Generation)和Young Space。

Old Space主要存放应用程序中生命周期长的存活对象;

Eden(伊甸园)主要存放新生的对象;

S0和S1是两个大小相同的内存区域,主要存放每次垃圾回收后Eden存活的对象,作为对象从Eden过渡到Old Space的缓冲地带(S是指英文单词Survivor Space)。

堆之所以要划分区间,是为了方便对象创建和垃圾回收,后面垃圾回收部分会解释。

2).线程内存区

线程内存区=单个线程内存+单个线程内存+.......

单个线程内存=PC Regster+JVM栈+本地方法栈

JVM栈=栈帧+栈帧+.....

栈帧=局域变量区+操作数区+帧数据区

在Java中,一个线程会对应一个JVM栈(JVM Stack),JVM栈里记录了线程的运行状态。

JVM栈以栈帧为单位组成,一个栈帧代表一个方法调用。栈帧由三部分组成:局部变量区、操作数栈、帧数据区。

(1)局部变量区

局部变量区,可以理解为一个以数组形式进行管理的内存区,从0开始计数,每个局部变量的空间是32位的,即4字节。

基本类型byte、char、short、boolean、int、float及对象引用等占一个局部变量空间,类型为short、byte和char的值在存入数组前要被转换成int值;long、double占两个局部变量空间,在访问long和double类型的局部变量时,只需要取第一个变量空间的索引即可,。

例如:

1
2
3
4
5
6
7
public static int runClassMethod(int i,long l,float f,double d,Object o,byte b) {   
    return 0;   
}   
            
public int runInstanceMethod(char c,double d,short s,boolean b) {   
    return 0;   
}

对应的局域变量区是:

runInstanceMethod的局部变量区第一项是个reference(引用),它指定的就是对象本身的引用,也就是我们常用的this,但是在runClassMethod方法中,没这个引用,那是因为runClassMethod是个静态方法。

(2)操作数栈

操作数栈和局部变量区一样,也被组织成一个以字长为单位的数组。但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。操作数栈是临时数据的存储区域。

例如:

1
2
3
int a= 100;
int b =5;
int c = a+b;

对应的操作数栈变化为:

从图中可以得出:操作数栈其实就是个临时数据存储区域,它是通过入栈和出栈来进行操作的。

PS:JVM实现里,有一种基于栈的指令集(Hotspot,oracle JVM),还有一种基于寄存器的指令集(DalvikVM,安卓 JVM),两者有什么区别的呢?

基于栈的指令集有接入简单、硬件无关性、代码紧凑、栈上分配无需考虑物理的空间分配等优势,但是由于相同的操作需要更多的出入栈操作,因此消耗的内存更大。 而基于寄存器的指令集最大的好处就是指令少,速度快,但是操作相对繁琐。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.demo3;
 
public class Test {
 
    public static void foo() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 5;
    }
 
    public static void main(String[] args) {
        foo();
    }
}

基于栈的Hotspot的执行过程如下:

基于寄存器的DalvikVM执行过程如下所示:

上述两种方式最终通过JVM执行引擎,CPU接收到的汇编指令是:

(3)帧数据区

 帧数据区存放了指向常量池的指针地址,当某些指令需要获得常量池的数据时,通过帧数据区中的指针地址来访问常量池的数据。此外,帧数据区还存放方法正常返回和异常终止需要的一些数据。

2.垃圾回收机制

1)、为什么要垃圾回收

JVM自动检测和释放不再使用的内存,提高内存利用率。 

Java 运行时JVM会执行 GC,这样程序员不再需要显式释放对象。 

2)、垃圾回收(GC)的分类

Minor GC(次要回收)

Full GC(主要回收)

3)、垃圾回收(GC)的产生过程

(1)新生成的对象在Eden区完成内存分配;

(2)当Eden区满了,再创建对象,会因为申请不到空间,触发minorGC,进行young(eden+1survivor)区的垃圾回收。(为什么是eden+1survivor:两个survivor中始终有一个survivor是空的,空的那个被标记成To Survivor);

(3)minorGC时,Eden不能被回收的对象被放入到空的survivor(也就是放到To Survivor,同时Eden肯定会被清空),另一个survivor(From Survivor)里不能被GC回收的对象也会被放入这个survivor(To Survivor),始终保证一个survivor是空的。(MinorGC完成之后,To Survivor 和 From Survivor的标记互换);

(4)当做第3步的时候,如果发现存放对象的那个survivor满了,则这些对象被copy到old区,或者survivor区没有满,但是有些对象已经足够Old(通过XX:MaxTenuringThreshold参数来设置),也被放入Old区;

(5)当Old区被放满的之后,进行完整的垃圾回收,即 Full GC;

(6)Full GC,整理Old Space里的对象时,把仍存活的对象放入到Old Space里。

JDK官方监控工具visualvm中visual gc插件图:

4)、什么情况下触发垃圾回收

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Minor GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC因为需要对整个堆进行回收,包括Young、Old和Perm,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。

有如下原因可能导致Full GC:

  • 年老代(Tenured)被写满

  • 持久代(Perm)被写满

  • System.gc()被显示调用

  • 上一次GC之后Heap的各域分配策略动态变化

5)、垃圾回收典型算法

(1).Mark-Sweep(标记-清除)算法

(2).Copying(复制)算法

(3).Mark-Compact(标记-整理)算法

(4).Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

目前大部分垃圾收集器对于新生代都采取Copying算法。而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

6)、垃圾收集器

垃圾收集算法是内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。

(1).Serial/Serial Old

Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是Copying算法,Serial Old收集器是针对老年代的收集器,采用的是Mark-Compact算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。

(2).ParNew

ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

(3).Parallel Scavenge

Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。

(4).Parallel Old

Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。

(5).CMS

CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。

(6).G1

G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

3.JVM参数

1).堆

-Xmx:最大堆内存

-Xms:初始时堆内存

-XX:MaxNewSize:最大年轻区内存

-XX:NewSize:初始时年轻区内存

-XX:MaxPermSize:最大持久带内存

-XX:PermSize:初始时持久带内存

2).栈

-xss:设置每个线程的堆栈大小

3).垃圾回收

4.堆 VS 栈

JVM栈是运行时的单位,而JVM堆是存储的单位。

JVM栈代表了处理逻辑,而JVM堆代表了数据。

JVM堆中存的是对象。JVM栈中存的是基本数据类型和JVM堆中对象的引用。

JVM堆是所有线程共享,JVM栈是线程独有。

PS:Java中的参数传递是传值呢?还是传址?

我们都知道:C 语言中函数参数的传递有:值传递,地址传递,引用传递这三种形式。但是在Java里,方法的参数传递方式只有一种:值传递。所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响。

要说明这个问题,先要明确两点:

1.引用在Java中是一种数据类型,跟基本类型int等等同一地位。

2.程序运行永远都是在JVM栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。

在运行JVM栈中,基本类型和引用的处理是一样的,都是传值。如果是传引用的方法调用,可以理解为“传引用值”的传值调用,即“引用值”被做了一个复制品,然后赋值给参数,引用的处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用值,被程序解释(或者查找)到JVM堆中的对象,这个时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是JVM堆中的数据。所以这个修改是可以保持的了。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.demo3;
 
public class DataWrap {
    public int a;
    public int b;
}
 
package com.demo3;
 
public class ReferenceTransferTest {
    public static void swap(DataWrap dw) {
        int tmp = dw.a;
        dw.a = dw.b;
        dw.b = tmp;
    }
 
    public static void main(String[] args) {
        DataWrap dw = new DataWrap();
        dw.a = 6;
        dw.b = 9;
        swap(dw);
    }
}

对应的内存图:

 

附:JVM详细图