内存分配与回收策略

以下例子使用 openjdk8 测试

一、对象优先在 Eden 分配

大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。

  • 虚拟机参数
    • -Xms20M -Xmx20M : 限制堆的大小为 20M
    • -Xmn10M :分配 10M 给新生代
    • -XX:SurvivorRatio=8 : 新生代中 Eden 区与一个 Survivor 区的空间比例是 8:1:1
    • -XX:+PrintGCDetails : 收集器日志参数,虚拟机在发生垃圾收集行为时打印内存回收日志
    • -XX:+UseSerialGC : 使用 Serial 收集器
  • 代码清单
  1. /**
  2. * @Description: 优先分配Eden
  3. * <p>
  4. * 虚拟机参数: 使用Serial加SerialOld客户端默认收集器组合下的内存分配和回收的策略(-XX:+UseSerialGC)
  5. *
  6. <pre>
  7. * -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
  8. * </pre>
  9. *
  10. * @Author hdj
  11. * @Date 2021/1/10 下午5:01
  12. */
  13. public class AllocationEden {
  14. public static final int _1M = 1024 * 1024;
  15. public static void main(String[] args) {
  16. byte[] allocation1, allocation2, allocation3, allocation4;
  17. allocation1 = new byte[2 * _1M];
  18. allocation2 = new byte[2 * _1M];
  19. allocation3 = new byte[2 * _1M];
  20. //出现一次minor GC
  21. allocation4 = new byte[4 * _1M];
  22. }
  23. }
  • 输出的 GC 日志
    1. [GC (Allocation Failure) [DefNew: 7996K->469K(9216K), 0.0051882 secs] 7996K->6613K(19456K), 0.0052237 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
    2. Heap
    3. def new generation total 9216K, used 4647K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    4. eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
    5. from space 1024K, 45% used [0x00000000ff500000, 0x00000000ff5755d8, 0x00000000ff600000)
    6. to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
    7. tenured generation total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    8. the space 10240K, 60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
    9. Metaspace used 3169K, capacity 4496K, committed 4864K, reserved 1056768K
    10. class space used 342K, capacity 388K, committed 512K, reserved 1048576K

从 GC 的日志可以看出 新生代总内存:9216K,已使用:4647K

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

大对象就是指需要大量连续内存空间的 Java 对象,最典型的大对象便是那种很长的字符串,或者元素数量很庞大的数组。大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,我们写程序的时候应注意避免。在 Java 虚拟机中要避免大对象的原因是,在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复制对象时,大对象就意味着高额的内存复制开销。

  • 虚拟机参数
    • -XX:PretenureSizeThreshold: 指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在 Eden 区及两个 Survivor 区之间来回复制,产生大量的内存复制操作。注意:该参数只对 Serial 和 ParNew 两款新生代收集器有效
    • -verbose:gc
    • Xms20M -Xmx20M -Xmn10M : 限制堆的大小为 20M, 新生代 10M
    • -XX:+UseSerialGC
    • -XX:+PrintGCDetails -XX:SurvivorRatio=8
  • 代码

    1. /**
    2. * @Description: PretenureSizeThreshold 测试
    3. * <p>
    4. * jvm args :
    5. * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC
    6. * -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
    7. * @Author hdj
    8. * @Date 2021/2/1 下午11:10
    9. */
    10. public class PretenureSizeThresholdTest {
    11. public static final int _1M = 1024 * 1024;
    12. public static void main(String[] args) {
    13. byte[] allocate;
    14. allocate = new byte[4 * _1M];
    15. }
    16. }
  • 输出的 GC 日志

    1. Heap
    2. def new generation total 9216K, used 2017K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    3. eden space 8192K, 24% used [0x00000000fec00000, 0x00000000fedf84d8, 0x00000000ff400000)
    4. from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
    5. to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
    6. tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    7. the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
    8. Metaspace used 3167K, capacity 4496K, committed 4864K, reserved 1056768K
    9. class space used 342K, capacity 388K, committed 512K, reserved 1048576K
  • 有 GC 日志可以看出 tenured generation 使用了 4096k,分配的 4M 字节数组直接被分配在老年代。

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

HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器,存储在对象头中。对象通常在 Eden 区里诞生,如果经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,该对象会被移动到 Survivor 空间中,并且将其对象年龄设为 1 岁。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过以下虚拟机参数设置。

  • 虚拟机参数
    • -XX:MaxTenuringThreshold : 设置对象晋升老年代的年龄阈值
  • 代码 ```java /**
    • @Description: 设置对象晋升老年代的年龄阈值 *
    • 虚拟机参数:
    • -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
  • @Author hdj

    • @Date 2021/2/1 下午11:29 */ public class TenuringThresholdTest {

      public static final int _1M = 1024 * 1024; public static void main(String[] args) {

      1. byte[] allocate, allocate2, allocate3;
      2. allocate = new byte[_1M / 4];
      3. //什么时候进入老年代取决于XX:MaxTenuringThreshold设置
      4. allocate2 = new byte[_1M * 4 ];
      5. allocate3 = new byte[_1M * 4 ];
      6. allocate3 = null;
      7. allocate3 = new byte[_1M * 4];

      } } ```

  • 设置-XX:MaxTenuringThreshold=15 ``` [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15)
  • age 1: 742936 bytes, 742936 total : 6204K->725K(9216K), 0.0043667 secs] 6204K->4821K(19456K), 0.0043995 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) : 4821K->0K(9216K), 0.0014240 secs] 8917K->4812K(19456K), 0.0014512 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 4812K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 47% used [0x00000000ff600000, 0x00000000ffab3390, 0x00000000ffab3400, 0x0000000100000000) Metaspace used 3167K, capacity 4496K, committed 4864K, reserved 1056768K class space used 342K, capacity 388K, committed 512K, reserved 1048576K ```

  • 设置-XX:MaxTenuringThreshold=1 ``` [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1)

  • age 1: 742936 bytes, 742936 total : 6204K->725K(9216K), 0.0046998 secs] 6204K->4821K(19456K), 0.0047442 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) : 4821K->0K(9216K), 0.0015807 secs] 8917K->4812K(19456K), 0.0016155 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 4812K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 47% used [0x00000000ff600000, 0x00000000ffab3390, 0x00000000ffab3400, 0x0000000100000000) Metaspace used 3166K, capacity 4496K, committed 4864K, reserved 1056768K class space used 342K, capacity 388K, committed 512K, reserved 1048576K ```

对于上面的代码,不管设置-XX:MaxTenuringThreshold=1 或者 15,都在第二次 Minor GC 后,都进入了老年代。

这里就有个疑问了,不是设置-XX:MaxTenuringThreshold 进入老年代的阀值吗,为什么不起作用呢?

  • 这里就涉及一个新的概念: 动态对象年龄判定——如果在 Survivor 空间中【低或等于某个年龄的】所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold 中要求的年龄,就可以直接晋升老年代。

看了动态对象年龄判定的简单解析,是不是感觉还有一个疑问,分配的 allocate 256K 没有超过 Survivor 空间的一半啊? 下面分析以下内存变化

  • -Xms20M -Xmx20M -Xmn10M 堆内存分配了 20M,新生代 10M,老年代 10M
  • -XX:SurvivorRatio=8 新生代比例 8:1:1,即 eden:8M ,survivor from : 1M , survivor to:1M
  • allocate 分配 256K,allocate2 分配 4M,此时 eden 应该占用 4M + 256K ,但是实际还有其它,猜测是 Java 内部创建的对象占用(不知道是哪些? 知道的大佬,请告知小弟)。
  • 这时 allocate3 分配 4M, 由于 eden 内存不够分配,触发一次 MinorGC,allocate2 晋升老年代,allocate 进入 survivor,但此时看 设置-XX:MaxTenuringThreshold=15的 GC 日志 6204K->725K(9216K) ,新生代 还有 725K 占用,所以除了 allocate 256K, 还有其他对象占用。 最后 allocate3 成功分配 4M 到 eden。
  • eden 占用 4M ,设置 allocate3 = null,又准备重新分配 allocate3=4M,eden 内存再次不够分配,又触发一次 MinorGC,由于 allocate3 置空,分配在 eden 的对象因为没有 GC Roots 引用,被垃圾回收掉,survivor 中被占用 725K,满足动态对象年龄判定条件,也晋升老年代 ,从 GC 日志 4821K->0K(9216K)中可以看出,最后 allocate3 成功分配 4M 到 eden。

四、动态对象年龄判定

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

  • https://github.com/fenixsoft/jvm_book/issues/13
  • -XX:TargetSurvivorRatio : 设定 survivor 区的目标使用率。默认 50,即 survivor 区对象目标使用率为 50%,即设置 desired_survivor_size

    1. size_t G1Policy::desired_survivor_size(uint max_regions) const {
    2. size_t const survivor_capacity = HeapRegion::GrainWords * max_regions;
    3. return (size_t)((((double)survivor_capacity) * TargetSurvivorRatio) / 100);
    4. }
  • jdk 源码, 计算进入老年代年龄阀值 ```c //编译的jdk13源码 uint AgeTable::compute_tenuring_threshold(size_t desired_survivor_size) { uint result;

    if (AlwaysTenure || NeverTenure) { assert(MaxTenuringThreshold == 0 || MaxTenuringThreshold == markOopDesc::max_age + 1,

    1. "MaxTenuringThreshold should be 0 or markOopDesc::max_age + 1, but is " UINTX_FORMAT, MaxTenuringThreshold);

    result = MaxTenuringThreshold; } else { size_t total = 0; uint age = 1; assert(sizes[0] == 0, “no objects with age zero should be recorded”); while (age < table_size) {

    1. total += sizes[age];
    2. // check if including objects of age 'age' made us pass the desired
    3. // size, if so 'age' is the new threshold
    4. if (total > desired_survivor_size) break;
    5. age++;

    } result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold; }

log_debug(gc, age)(“Desired survivor size “ SIZE_FORMAT “ bytes, new threshold “ UINTX_FORMAT “ (max threshold “ UINTX_FORMAT “)”, desired_survivor_size * oopSize, (uintx) result, MaxTenuringThreshold);

return result; }

  1. - 测试 动态年龄设置

/**

  • @Description: 动态对象年龄判定 *
  • 虚拟机参数:
  • -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
  • @Author hdj
  • @Date 2021/2/1 下午11:29 */ public class TenuringThresholdTest2 {

    public static final int _1M = 1024 * 1024;

    public static void main(String[] args) {

    1. byte[] allocate, allocate2, allocate3, allocate4;
    2. //内部占用 460K左右,注释allocate 和 allocate2的赋值, 在执行一次 MinorGC后,查看GC日志 5781K->461K(9216K)
    3. //因为这两个对象加起来已经到达了70K + 461K > 512K,并且它们是同年龄的,满足同年龄对象达到Survivor空间一半的规则
    4. //所以进入了老年代
    5. allocate = new byte[1024 * 35];
    6. allocate2 = new byte[1024 * 35];
    7. allocate3 = new byte[_1M * 4];
    8. allocate4 = new byte[_1M * 4];
    9. allocate4 = null;
    10. allocate3 = new byte[_1M * 4];

    } } ```

  • 没有注释 allocate2 变量赋值运行 ``` [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15)
  • age 1: 552488 bytes, 552488 total : 5948K->539K(9216K), 0.0034109 secs] 5948K->4635K(19456K), 0.0034457 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) : 4635K->0K(9216K), 0.0009382 secs] 8731K->4626K(19456K), 0.0009562 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000) from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 4626K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 45% used [0x00000000ff600000, 0x00000000ffa84ba0, 0x00000000ffa84c00, 0x0000000100000000) Metaspace used 3167K, capacity 4496K, committed 4864K, reserved 1056768K class space used 342K, capacity 388K, committed 512K, reserved 1048576K ```

  • 注释没有注释 allocate2 变量赋值再运行 ``` [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15)

  • age 1: 491176 bytes, 491176 total : 5948K->479K(9216K), 0.0031022 secs] 5948K->4575K(19456K), 0.0031245 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15)
  • age 1: 144 bytes, 144 total
  • age 2: 482256 bytes, 482400 total : 4659K->471K(9216K), 0.0014258 secs] 8756K->4567K(19456K), 0.0014492 secs] [Times: user=0.00 sys=0.01, real=0.00 secs] Heap def new generation total 9216K, used 4950K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) eden space 8192K, 54% used [0x00000000fec00000, 0x00000000ff05fbf0, 0x00000000ff400000) from space 1024K, 46% used [0x00000000ff400000, 0x00000000ff475c60, 0x00000000ff500000) to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000) tenured generation total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) the space 10240K, 40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000) Metaspace used 3158K, capacity 4496K, committed 4864K, reserved 1056768K class space used 340K, capacity 388K, committed 512K, reserved 1048576K ```

五、空间分配担保

在发生 Minor GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间

  • 如果这个条件成立,那这一次 Minor GC 可以确保是安全的。
  • 如果不成立
    • 则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);
    • 如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,
    • 如果大于,将尝试进行一次 Minor GC,尽管这次 Minor GC 是有风险的;
    • 如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC
  • 虚拟机参数
    • -XX:HandlePromotionFailure: 设置值是否允许担保失败(Handle Promotion Failure); JDK 6Update 24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC。
  • hotpot 空间分配检查代码

    1. bool TenuredGeneration::promotion_attempt_is_safe(size_t max_promotion_in_bytes) const {
    2. //老年代最大可用连续空间
    3. size_t available = max_contiguous_available();
    4. //每次晋升老年代的平均大小
    5. size_t av_promo = (size_t)gc_stats()->avg_promoted()->padded_average();
    6. // 老年代可用空间是否大于平均晋升大小,或者老年代可用空间是否大于当此GC时新生代所有对象容量
    7. bool res = (available >= av_promo) || (available >= max_promotion_in_bytes);
    8. log_trace(gc)("Tenured: promo attempt is%s safe: available(" SIZE_FORMAT ") %s av_promo(" SIZE_FORMAT "), max_promo(" SIZE_FORMAT ")",
    9. res? "":" not", available, res? ">=":"<", av_promo, max_promotion_in_bytes);
    10. return res;
    11. }
  • 空间分配担保实践

    1. /**
    2. * @Description: 空间分配担保 请在 JDK 6Update 24 之前运行
    3. * <p>
    4. * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure
    5. * @Author hdj
    6. * @Date 2021/2/28 下午12:38
    7. */
    8. public class HandlePromotionFailureTest {
    9. public static final int _1M = 1024 * 1024;
    10. public static void main(String[] args) {
    11. byte[] allocate1;
    12. byte[] allocate2;
    13. byte[] allocate3;
    14. byte[] allocate4;
    15. byte[] allocate5;
    16. byte[] allocate6;
    17. byte[] allocate7;
    18. allocate1 = new byte[_1M * 2];
    19. allocate2 = new byte[_1M * 2];
    20. allocate3 = new byte[_1M * 2];
    21. allocate1 = null;
    22. allocate4 = new byte[_1M * 2];
    23. allocate5 = new byte[_1M * 2];
    24. allocate6 = new byte[_1M * 2];
    25. allocate4 = null;
    26. allocate5 = null;
    27. allocate6 = null;
    28. allocate7 = new byte[_1M * 2];
    29. }
    30. }

参考