[转]JVM运行时内存结构

[转]http://www.cnblogs.com/dolphin0520/p/3783345.html

目录[-]

1.为什么会有年轻代


     我们先来屡屡,为什么需要把堆分代?不分代不能完成他所做的事情么?其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

2.年轻代中的GC


    HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

    因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

    在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

3.一个对象的这一辈子


    我是一个普通的java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

4.有关年轻代的JVM参数


1)-XX:NewSize和-XX:MaxNewSize

   用于设置年轻代的大小,建议设为整个堆大小的1/3或者1/4,两个值设为一样大。

2)-XX:SurvivorRatio

   用于设置Eden和其中一个Survivor的比值,这个值也比较重要。

3)-XX:+PrintTenuringDistribution

   这个参数用于显示每次Minor GC时Survivor区中各个年龄段的对象的大小。

4).-XX:InitialTenuringThreshol和-XX:MaxTenuringThreshold

   用于设置晋升到老年代的对象年龄的最小值和最大值,每个对象在坚持过一次Minor GC之后,年龄就加1。

 --------------------------------------------------------------------------------------------------------------------------

http://my.oschina.net/sunchp/blog/369707?p=1#comments

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)、回收哪些内存区域

因为线程内存区随着线程的产生和退出而分配和回收,所以垃圾回收主要集中在共享内存区,也就是持久带(Permanent Space)和堆(Heap)

3)、如何判断对象已死 (对象标记)

(1)引用计数法

引用计数法就是通过一个计数器记录该对象被引用的次数,方法简单高效,但是解决不了循环引用的问题。比如对象A包含指向对象B的引用,对象B也包含指向对象A的引用,但没有引用指向A和B,这时当前回收如果采用的是引用计数法,那么对象A和B的被引用次数都为1,都不会被回收。JVM不是采用这种方法。

(2) 根搜索(可达性分析算法)

根搜索(可达性分析算法)可以解决对象循环引用的问题,基本原理是:通过一个叫“GC ROOT”根对象作为起点,然后根据关联关系,向下节点搜索,搜索路径叫引用链,也就是常说的引用。从“GC ROOT”根对象找不到任何一条路径与之相连的对象,就被判定可以回收,相当于这对象找不到家的感觉。

示例图:

GC会收集那些不是GC root且没有被GC root引用的对象。一个对象可以属于多个GC root。

GC root有几下种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象

  • 方法区中类静态属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中JNI(native方法)引用的对象

  • 用于JVM特殊目的对象,例如系统类加载器等等

虽然有可达性分析算法来判定对象状态,但这并不是对象是否被回收的条件,对象回收的条件远远比这个复杂。无法通过GC ROOT关联到的对象,不都是立刻被回收。如果这个对象没有被关联到,而且没有被mark2标记,那么会进入一个死缓的阶段,被第一次标记(mark1),然后被放入一个F-Queue队列;如果这个对象被mark2标记了,那么这个对象将会被回收。

F-Queue队列由一个优先级较低的Finalizer线程去执行,其中的mark1对象等待执行自己的finalize()方法(JVM并不保证等待finalize()方法运行结束,因为finalize() 方法或者执行慢,或者死循环,会影响该队列其他元素执行)。执行mark1对象的finalize()方法,就会进行第二次标记(mark2)。以后的GC都会按这个逻辑执行“搜索,标记1,标记2”。

这一“标记”过程是后续垃圾回收算法的基础。

PS:

  • 如果在finalize() 方法体内,再次对本对象进行引用,那么对象就复活了。

  • finalize()方法只会被执行一次,所以对象只有一次复活的机会。

3)垃圾回收算法

垃圾回收算法主要有三种:

  • 标记-清除

  • 标记-复制

  • 标记-整理

这三种都有“标记”过程,这个标记过程就是上述的根搜索(可达性分析算法)。后面的“清除”、“复制”和“整理”动作,是具体的对象被回收的实现方式。

(1)标记-清除

通过根搜索(可达性分析算法)标记完成后,直接将标记为垃圾的对象所占内存空间释放。这种算法的缺点是内存碎片多。

虽然缺点明显,这种策略却是后两种策略的基础。正因为它的缺点,所以促成了后两种策略的产生。

动图:

(2)标记-复制

通过根搜索(可达性分析算法)标记完成后,将内存分为两块,将一块内存中保留的对象全部复制到另

一块空闲内存中。

动图:

这种算法的缺点是,可用内存变成了一半。怎么解决这个缺点呢?

JVM将堆(heap)分成young区和old区。young区包括eden、s0、s1,并且三个区之间的大小有一定比例。例如,按8:1:1分成一块Eden和两小块Survivor区,每次GC时,young区里,将Eden和S0中存活的对象复制到另一块空闲的S1中。

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

Minor GC主要过程:

a、新生成的对象在Eden区完成内存分配;

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

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

d、当做第3步的时候,如果发现存放对象的那个survivor满了,则这些对象被copy到old区,或者survivor区没有满,但是有些对象已经足够Old(通过XX:MaxTenuringThreshold参数来设置),也被放入Old区。(对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会晋升到老年代中)

(3)标记-整理

old space也可以标记-复制策略吗?当然不行!

young space中的对象大部分都是生命周期较短的对象,每次GC后,所剩下的活对象数量不是很大。而old space中的对象大部分都是生命周期特别长的对象,即使GC后,仍然会剩下大量的活对象。如果仍然采用复制动作,回收效率会变得非常低。

根据old space的特点,可以采用整理动作。整理时,先清除掉应该清除的对象,然后把存活对象“压缩”到堆的一端,按顺序排放。

动图:

Old space(+Permanent Space)的垃圾回收是偶尔发生的,被称为Full GC(主要回收)。Full GC因为需要对整个堆进行回收,包括Young、Old和Perm,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。

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

  • 年老代(Tenured)被写满

  • 持久代(Perm)被写满

  • System.gc()被显示调用

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

4)、垃圾收集器

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

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

目前大部分垃圾收集器对于Young space都采取“标记-复制”算法。而由于Old space的特点是每次回收都只回收少量对象,一般使用的是“标记-整理”算法。

(1)Young Space上的GC实现:

Serial(串行): Serial收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。Serial收集器是针对新生代的收集器,采用的是“标记-复制”算法。它的优点是实现简单高效,但是缺点是会给用户带来停顿。这个收集器类型仅应用于单核CPU桌面电脑。使用serial收集器会显着降低应用程序的性能。

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

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

(2)Old Space上的GC实现:

Serial Old(串行):Serial收集器的Old Space版本,采用的是“标记-整理”算法。这个收集器类型仅应用于单核CPU桌面电脑。使用serial收集器会显着降低应用程序的性能。

Parallel Old(并行):Parallel Old是Parallel Scavenge收集器的Old Space版本(并行收集器),使用多线程和“标记-整理”算法。

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

(3).G1

G1(Garbage First)收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。还有一个特点之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代,老年代)。

3.JVM参数

1).堆

-Xmx:最大堆内存,如:-Xmx512m

-Xms:初始时堆内存,如:-Xms256m

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

-XX:NewSize:初始时年轻区内存.通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90%

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

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

-XX:+PrintGCDetails。打印 GC 信息

 -XX:NewRatio 新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3

 -XX:SurvivorRatio 新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10

2).栈

-xss:设置每个线程的堆栈大小. JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。

3).垃圾回收

4).JVM  client模式和server模式

Java_home/bin/java命令有一个-server和-client参数,该参数标识了JVM以server模式或client模式启动。

JVM Server模式与client模式启动,最主要的差别在于:-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器, 而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对彻底,,服务起来之后,性能更高。

(1)查看当前JVM默认启动模式

java -version 可以直接查看出默认使用的是client还是 server。

(2)JVM默认启动模式自动侦测

从JDK 5开始,如果没有显式地用-client或者-server参数,那么JVM启动时,会根据机器配置和JDK的版本,自动判断该用哪种模式。

  • the definition of a server-class machine is one with at least 2 CPUs and at least 2GB of physical memory.

  • windows平台,64位版本的JDK,没有提供-client模式,直接使用server模式。

(3).通过配置文件,改变JVM启动模式

两种模式的切换可以通过更改配置(jvm.cfg配置文件)来实现:

32位的JVM配置文件在JAVA_HOME/jre/lib/i386/jvm.cfg,

64位的JVM配置文件在JAVA_HOME/jre/lib/amd64/jvm.cfg, 目前64位只支持server模式。

例如:

32位版本的JDK 5的jvm.cfg文件内容:

1
2
3
4
5
6
-client KNOWN
-server KNOWN
-hotspot ALIASED_TO -client
-classic WARN
-native ERROR
-green ERROR

64位版本的JDK 7的jvm.cfg文件内容:

1
2
3
4
5
6
-server KNOWN
-client IGNORE
-hotspot ALIASED_TO -server
-classic WARN
-native ERROR
-green ERROR

 

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);
    }
}

对应的内存图:

 

附:

 

 

---------------------------------------------------------------------------------------------------------------------------

http://www.cnblogs.com/ggjucheng/p/3977384.html

前言 

JVM GC是JVM的内存回收算法,调整JVM GC(Garbage Collection),可以极大的减少由于GC工作,而导致的程序运行中断方面的问题,进而适当的提高Java程序的工作效率。但是调整GC是以个极为复杂的过程,所以我们要了解JVM内存组成,回收算法,对象分配机制。

 

JVM 堆内存组成

Java堆由Perm区和Heap区组成,Heap区由Old区和New区(也叫Young区)组成,New区由Eden区、From区和To区(Survivor)组成。


Eden区用于存放新生成的对象。Eden中的对象生命不会超过一次Minor GC。

Survivor Space  有两个,存放每次垃圾回收后存活的对象,即图的S0和S1。

Old Generation  Old区,也称老生代,主要存放应用程序中生命周期长的存活对象

 

JVM初始分配的内存由-Xms指定,JVM最大分配的内存由-Xmx指定。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小。

-XX:NewRatio= 参数可以设置Young与Old的大小比例,-server时默认为1:2,如果太小,会使大对象直接分配到old区去,增大major collections的执行的次数,影响性能。
-XX:SurvivorRatio= 参数可以设置Eden与Survivor的比例,默认为1:8,Survivio大了会浪费,如果小了的话,会使一些大对象在做minor gc时,直接从eden区潜逃到old区,让old区的gc频繁。这个参数保持默认就好了,一般情况下,对性能影响不大。

启动后可通过jmap –heap [pid]查看。
 
由于堆的整体大小是固定的,young generation越大,tenured generation越小,越会增加major collections的执行的次数。所以最佳的选择是由对象的生命周期分布所决定。

 

New区的Collector

 

1、  串行GC(Serial Copying)

     client模式下的默认GC方式,也可使用-XX:+UseSerialGC指定。

2、  并行回收GC(Parallel Scavenge)
     server模式下的默认GC方式,也可用-XX:+UseParallelGC强制指定。

     采用PS时,默认情况下JVM会在运行时动态调整Eden:S0:S1的比例,如果不希望自动调整可以使用-XX:-UseAdaptiveSizePolicy参数,内存分配和回收的算法和串行相同,唯一不同仅在于回收时为多线程。

3、  并行GC(ParNew)

     CMS GC时默认采用,也可以采用-XX:+UseParNewGC指定。内存分配、回收和PS相同,不同的仅在于会收拾会配合CMS做些处理。

 

Old区的几种Collector

1、  串行GC(Serial MSC)

     client模式下的默认GC方式,可通过-XX:+UseSerialGC强制指定。每次进行全部回收,进行Compact,非常耗费时间。

2、  并行GC(Parallel MSC)(备注,吞吐量大,但是gc的时候响应很慢)

    server模式下的默认GC方式,也可用-XX:+UseParallelGC=强制指定。可以在选项后加等号来制定并行的线程数。

3、  并发GC(CMS)线上环境采用的GC方式,也就是Realese环境的方式。(备注,响应比并行gc快很多,但是牺牲了一定的吞吐量)

     使用CMS是为了减少GC执行时的停顿时间,垃圾回收线程和应用线程同时执行,可以使用-XX:+UseConcMarkSweepGC=指定使用,后边接等号指定并发线程数。CMS每次回收只停顿很短的时间,分别在开始的时候(Initial Marking),和中间(Final Marking)的时候,第二次时间略长。具体CMS的过程可以参考相关文档。JStat中将Initial Mark和Remark都统计成了FGC。

CMS一个比较大的问题是碎片和浮动垃圾问题(Floating Gabage)。碎片是由于CMS默认不对内存进行Compact所致,可以通过-XX:+UseCMSCompactAtFullCollection。

 

总体来讲,Old区的大小较大,垃圾回收算法较费时间,导致较长时间的应用线程停止工作,而且需要进行Compact,所以不应该出现较多Major GC。Major GC的时间常常是Minor GC的几十倍。JVM内存调优的重点,减少Major GC 的次数,因为为Major GC 会暂停程序比较长的时间,如果Major GC 的次数比较多,意味着应用程序的JVM内存参数需要进行调整。

 

JVM内存分配策略

 

1. 对象优先在Eden分配


如果Eden区不足分配对象,会做一个minor gc,回收内存,尝试分配对象,如果依然不足分配,才分配到Old区。


2.大对象直接进入老年代


大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组,虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,

3.长期存活的对象将进入老年代

在经历了多次的Minor GC后仍然存活:在触发了Minor GC后,存活对象被存入Survivor区在经历了多次Minor GC之后,如果仍然存活的话,则该对象被晋升到Old区。
虚拟机既然采用了分代收集的思想来管理内存,那内存回收时就必须能识别哪些对象应当放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。

4.动态对象年龄判定


为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。


5.Minor GC后Survivor空间不足就直接放入Old区

6.空间分配担保

在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

 

JVM GC组合方式

 

 

如何监视GC

1.概览监视gc。

   jmap -heap [pid] 查看内存分布

   jstat -gcutil [pid] 1000 每隔1s输出java进程的gc情况

2.详细监视gc。

   在jvm启动参数,加入-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log。

   输入示例:

  

 [GC [ParNew: 11450951K->1014116K(11673600K), 0.8698830 secs] 27569972K->17943420K(37614976K), 0.8699520 secs] [Times: user=11.28 sys=0.82, real=0.86 secs]

   表示发生一次minor GC,ParNew是新生代的gc算法,11450951K表示eden区的存活对象的内存总和,1014116K表示回收后的存活对象的内存总和,11673600K是整个eden区的内存总和。0.8699520 secs表示minor gc花费的时间。

   27569972K表示整个heap区的存活对象总和,17943420K表示回收后整个heap区的存活对象总和,37614976K表示整个heap区的内存总和。

[Full GC [Tenured: 27569972K->16569972K(27569972K), 180.2368177 secs] 36614976K->27569972K(37614976K), [Perm : 28671K->28635K(28672K)], 0.2371537 secs]

  表示发生了一次Full GC,整个JVM都停顿了180多秒,输出说明同上。只是Tenured: 27569972K->16569972K(27569972K)表示的是old区,而上面是eden区。

 

更多可以参考 阿里分享的ppt sunjdk1.6gc.pptx

posted on 2015-11-07 15:20  blogsheng  阅读(419)  评论(0编辑  收藏  举报