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

对象的分配的细节取决于当前使用哪一种垃圾收集器组合,以及和内存相关参数有关,本文主要讨论 Serial/SerialOld 收集器的内存分配和回收的策略,其他几种垃圾收集器可以自己去探讨。
先介绍下 MinorGC 和 FullGC 的概念。
新生代 GC(MinorGC): 发生在新生代,Java 对象大多都有朝生夕死的特性,MinorGC 非常频繁,回收速度也比较快。
老年代 GC(MajorGC/FullGC): 发生在老年代,出现 MajorGC 经常至少伴随一次的 MinorGC,但非绝对。MajorGC 的速度一般比 MinorGC 慢 10 倍以上。
image.png
下面是最普遍的内存分配规则。

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。
image.png

jdk1.6 update24 之后的担保流程

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

/**
 * 空间分配担保
 * 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 在新生代。

https://alicharles.oss-cn-hangzhou.aliyuncs.com/static/images/mp_qrcode.jpg
文章目录
  1. 1、对象优先在 eden 分配
  2. 2、大对象直接进入老年代
  3. 3、长期存活对象将进入老年代
  4. 4、动态对象年龄判定
  5. 5、空间分配担保
    1. jdk1.6 update24 之前的担保流程
    2. jdk1.6 update24 之后的担保流程