JVM(三)内存分配与回收策略

对象的分配的细节取决于当前使用哪一种垃圾收集器组合,以及和内存相关参数有关,本文主要讨论Serial/SerialOld收集器的内存分配和回收的策略,其他几种垃圾收集器可以自己去探讨。

先介绍下MinorGC和FullGC的概念。

新生代GC(MinorGC): 发生在新生代,Java对象大多都有朝生夕死的特性,MinorGC非常频繁,回收速度也比较快。

老年代GC(MajorGC/FullGC): 发生在老年代,出现MajorGC经常至少伴随一次的MinorGC,但非绝对。MajorGC 的速度一般比MinorGC慢10倍以上。

20160726010549 41753

下面是最普遍的内存分配规则。

1、对象优先在eden分配

/**
 * 对象优先在Eden分配
 * vm参数,新生代10M, eden区8M,surivior区1M(from,to)
 * -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:+PrintGCDetails
 */
public static void testAllocation() {
    byte[] allocation1, allocation2 ,allocation3 ,allocation4;
    allocation1 = new byte[2 * _1MB];
    allocation2 = new byte[2 * _1MB];
    allocation3 = new byte[2 * _1MB];
    allocation4 = new byte[4 * _1MB]; //出现一次Minor GC
}

使用串行垃圾回收,新生代10M, eden区8M,surivior区1M(from,to)

从输出结果看 执行allocation4 = new byte[4 * _1MB];会发生一次GC,GC的结果是7458K→601K,而总内存占用量几乎没有减少,因为allocation1,allocation2,allocation3都是存活,发现Eden区已经占用了6M,剩余空间不足以分配allocation4 的4M的空间,因此发生MinorGC,GC期间发现已有的3个2MB大小的对象无法放入到Survivor空间(只有1M大小),所以只好通过分配担保机制提前转移到老年代中去。

GC结束后, eden被占用4M(allocation4),survivor空闲,老年代占用6M (allocation1,allocation2,allocation3) 。

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

虚拟机提供-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样做可以避免在Eden和两个Survivor区域之间发生大量的内存复制操作

/**
 * 大对象直接进入老年代
 * vm参数,新生代10M, eden区8M,surivior区1M(from,to)
 * -XX:PretenureSizeThreshold=3145728 -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC
 */
public static void testPretenureSizeThreshold() {
    byte[] allocation;
    allocation = new byte[4 * _1MB]; //直接分配在老年代(大于3M)
}

PretenureSizeThreshold参数在UseParallelGC或者UseG1GC的时候都是不起作用的,只在Serial和ParNew新生代收集器中有效。**

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

对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置,当对象达到这个年龄后就将进入老年代。

如果对象在Eden出生并且经过第一次MinorGC后仍然存活,并且能够被Survivor容纳的话,被移动到Survivor空间中,并将对象年龄设置成1,对象在Survivor中每熬过一次MinorGC,年龄就增加1岁。年龄到一定程度(默认为15岁),就会被晋升到到老年代中。可以通过参数-XX:MaxTenuringThreshold设置。

/**
 * 长期存活的对象进入老年代
 * vm参数,新生代10M, eden区16M,surivior区2M(from,to)
 * -XX:MaxTenuringThreshold=1 -Xms40M -Xmx40M -Xmn20M -XX:SurvivorRatio=8 -XX:+UseSerialGC  -XX:+PrintTenuringDistribution -XX:+PrintGCDetails
 */
public static void testTenuringThreshold() {
    byte[] allocation1, allocation2 ,allocation3;
    allocation1 = new byte[1/2 * _1MB];
    // 什么时候进入老年代取决于MaxTenuringThreshold的设置
    allocation2 = new byte[8 * _1MB];
    allocation3 = new byte[8 * _1MB];
    allocation3 = null;
    allocation3 = new byte[8 * _1MB];
}

-XX:MaxTenuringThreshold=1 -XX:MaxTenuringThreshold=15 发生了两次Minor GC,第一次是在给allocation3进行分配的时候会出现一次Minor GC,此时survivor区域不能容纳allocation2,但是可以容纳allocation1,所以allocation1将会进入survivor区域并且年龄为1,达到了阈值,将在下一次GC时晋升到老年代,而allocation2则会通过担保机制进入老年代。第二次发生GC是在第二次给allocation3分配空间时,这时,allocation1的年龄加1,晋升到老年代,此次GC也可以清理出原来allocation3占据的4MB空间,将allocation3分配在Eden区。所以,最后的结果是allocation1、allocation2在老年代,allocation3在Eden区。

4、动态对象年龄判定

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

/**
 * 动态对象年龄判断
 * vm参数,新生代10M, eden区16M,surivior区2M(from,to)
 * -Xms40M -Xmx40M -Xmn20M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:+PrintTenuringDistribution -XX:+PrintGCDetails
 */
public static void testDynamicTenuringThreshold() {
    byte[] allocation1, allocation2 ,allocation3,allocation4;
    allocation1 = new byte[_1MB / 2];
    allocation2 = new byte[_1MB / 2];
    // allocation1 + allocation2 大于surivior空间的一半
    allocation3 = new byte[8 * _1MB];
    allocation4 = new byte[8 * _1MB];
    allocation4 = null;
    allocation4 = new byte[8 * _1MB];
}

发生了两次Minor GC,第一次发生在给allocation4分配内存时,此时allocation1、allocation2将会进入survivor区,而allocation3通过担保机制将会进入老年代。第二次发生在给allocation4分配内存时,此时,survivor区的allocation1、allocation2达到了survivor区容量的一半,将会进入老年代,此次GC可以清理出allocation4原来的4MB空间,并将allocation4分配在Eden区。最终,allocation1、allocation2、allocation3在老年代,allocation4在Eden区。

5、空间分配担保

jdk1.6 update24之前的担保流程

在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则进行一次Minor GC。如果小于或者没有设置HandlePromotionFailure,则要进行一次Full GC。

取平均值进行比较其实仍然是一种动态概率的手段,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败。如果出现了HandlePromotionFailure失败,则会重新发起一次Full GC,大部分情况都会将HandlePromotionFailure打开,避免过于频繁的Full GC。

20160726010616 11996

jdk1.6 update24之后的担保流程

在jdk1.6 update24之后,HandlePromotionFailure参数不会影响虚拟机空间分配担保策略,虚拟机改为,只要老年代最大连续空间大于新生代对象总和或者大于历次晋升平均大小,都将进行minor gc,否则将进行Full gc。

20160726010632 98673
/**
 * 空间分配担保
 * vm参数,新生代10M, eden区8M,surivior区1M(from,to)
 * -XX:HandlePromotionFailure
 * -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintTenuringDistribution -XX:SurvivorRatio=8
 */
public static void testHandlePromotionFailure() {
    byte[] allocation1, allocation2 ,allocation3,allocation4, allocation5, allocation6 ,allocation7;
    allocation1 = new byte[2 * _1MB];
    allocation2 = new byte[2 * _1MB];
    allocation3 = new byte[2 * _1MB];
    allocation1 = null;
    allocation4 = new byte[2 * _1MB];
    allocation5 = new byte[2 * _1MB];
    allocation6 = new byte[2 * _1MB];
    allocation4 = null;
    allocation5 = null;
    allocation6 = null;
    allocation7 = new byte[2 * _1MB];
}

发生了两次GC,第一次发生在给allocation4分配内存空间时,由于老年代的连续可用空间大于存活的对象总和, 所以allocation2、allocation3将会进入老年代,allocation1的空间将被回收,allocation4分配在新生代;第二次发生在给allocation7分配内存空间时,此次GC将allocation4、allocation5、allocation6所占的内存全部回收。最后,allocation2、allocation3在老年代,allocation7在新生代。

文章目录
  1. 1、对象优先在eden分配
  2. 2、大对象直接进入老年代
  3. 3、长期存活对象将进入老年代
  4. 4、动态对象年龄判定
  5. 5、空间分配担保
    1. jdk1.6 update24之前的担保流程
    2. jdk1.6 update24之后的担保流程