JVM学习-java垃圾回收-内存分配

Posted 智公博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM学习-java垃圾回收-内存分配相关的知识,希望对你有一定的参考价值。

在上一篇中主要说到了垃圾回收算法、垃圾收集器等,这篇我们来学习下对象在内存分配的策略;

一、概述

对象在内存分配,一般都是在堆内存上分配,主要是会分配到新生代的Eden区,如果有开启本地线程分配缓冲区,会按线程优先在TLAB上分配,还有少数情况会直接在老年代分配;

对象内存的分配的策略细节由垃圾收集器组合决定,还有许多虚拟机参数可以由我们控制设置;这里我们主要看下几天普遍的分配规则,并在 Serial + Serial Old 组合验证下,其他组合的垃圾收集器也基本相识;

二、对象优先在Eden分配

一般情况下,对象在新生代Eden区分配,当Eden区空间不足是,虚拟机可能会发起一次Minor GC;

Minor GC - 新生代GC,指发送在新生代的GC,非常频繁,回收速度较快
Major GC(Full GC) - 老年代GC,出现Major GC 一般都伴随至少一次的Minor GC(不是一定),而且Major GC 一般都比Minor GC 要慢的多;

通过测试代码验证下这个规则:

    private static final int _1MB = 1024 * 1024;
    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
     */
    public static void testEdenAllocation() 
        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
     

通过VM设置 堆内存大小为20M,老年代10M,Eden区与一个Survivor区的比例是 8:1;所以新生代可用空间应该为9M(9216K)(Eden + 一个Survivor),两个Survivor区分别占用1M;
测试代码中,开始创建3个2M对象,此时新生代可用空间应该剩余3M,当创建最后一个4M的对象时,可用空间不足,会发生一次Minor GC,开始创建的3个2M对象不会被回收,而是被移到老年代;代码执行完时,新生代使用了4M,而老年代使用了6M;

程序运行完打印出来的GC日志可以说明:

[GC[DefNew: 6824K->472K(9216K), 0.0071169 secs] 6824K->6616K(19456K), 0.0071828 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 4896K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 8192K,  54% used [0x00000000f9a00000, 0x00000000f9e51fa0, 0x00000000fa200000)
  from space 1024K,  46% used [0x00000000fa300000, 0x00000000fa376348, 0x00000000fa400000)
  to   space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
 tenured generation   total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2551K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  12% used [0x00000000fae00000, 0x00000000fb07de20, 0x00000000fb07e000, 0x00000000fc2c0000)
No shared spaces configured.

从GC日志说明,
- Eden区空间8M 使用4M(eden space 8192K, 54% used)
- 两个Survivor空间各占1M空间,(from space 1024K, 46% used )(to space 1024K, 0% used )
- 新生代空间9M,使用4M( def new generation total 9216K, used 4896K)
- 老年代空间1M,使用6M(tenured generation total 10240K, used 6144K)

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

所谓大对象即是需要占用较大连续内存空间的对象,如超长的字符串,大容量基本类型数组等;大对象对于虚拟机的内存分配是不利的,特别是大量的“临时”大对象,虚拟机可能为了给这个对象分配内存而需要更加频繁的GC;

在Serial 和 ParNew 收集器下,虚拟机可以通过-XX:PretenureSizeThreshold参数设置对象直接进入老年代分配的阀值;大对象直接进入老年代可以避免在Eden区和两个Survivor区发生大量的内存复制;

通过以下测试代码验证,设置-XX:PretenureSizeThreshold=3145728 (这个参数单位是 byte)

    private static final int _1MB = 1024 * 1024;
    /**
     * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
     * -XX:SurvivorRatio=8 -XX:+UseSerialGC
     * -XX:PretenureSizeThreshold=3145728
     */
    public static void testBigObjAllocation() 
        byte[] allocation;
        allocation = new byte[4 * _1MB];
    

如果参数有效,测试代码中创建一个4M的对象,应该直接在老年代分配,通过GC日志验证:

Heap
 def new generation   total 9216K, used 999K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 8192K,  12% used [0x00000000f9a00000, 0x00000000f9af9f70, 0x00000000fa200000)
  from space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
 tenured generation   total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2548K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb07d388, 0x00000000fb07d400, 0x00000000fc2c0000)
No shared spaces configured.

可以看出,老年代占用了4M (tenured generation total 10240K, used 4096K)

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

虚拟机为每个对象都保存一个年龄计算器,如果对象在Eden区被首次分配,然后进过一次Minor GC后然后可以存活,并且Survivor有足够空间区容纳,将被迁移到Survivor区,对象年龄加一;
对象进入老年代的年龄阀值,可以通过参数 -XX:MaxTenuringThreshold 设置;为了测试这个规则以及参数,我们设置-XX:MaxTenuringThreshold 值为1和10进行测试:

    /**
     * -verbose:gc -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails
     * -XX:SurvivorRatio=8 -XX:+UseSerialGC
     * -XX:MaxTenuringThreshold=1(10)
     */
    public static void testTrnuringThreshold() 
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[ _1MB / 8];
        allocation2 = new byte[8 * _1MB];
        System.out.println("after all 2");
        allocation3 = new byte[8 * _1MB];
        System.out.println("after all 3");
        allocation3 = null;
        allocation3 = new byte[8 * _1MB];
        System.out.println("after all 4");
    

当参数值为1时,得到结果:

after all 2
[GC[DefNew: 9303K->602K(18432K), 0.0043713 secs] 9303K->8794K(38912K), 0.0044024 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] 
after all 3
[GC[DefNew: 9466K->0K(18432K), 0.0011212 secs] 17658K->8793K(38912K), 0.0011374 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
after all 4
Heap
 def new generation   total 18432K, used 8472K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,  51% used [0x00000000f8600000, 0x00000000f8e460f8, 0x00000000f9600000)
  from space 2048K,   0% used [0x00000000f9600000, 0x00000000f9600140, 0x00000000f9800000)
  to   space 2048K,   0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9a00000)
 tenured generation   total 20480K, used 8793K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,  42% used [0x00000000f9a00000, 0x00000000fa296598, 0x00000000fa296600, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2552K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  12% used [0x00000000fae00000, 0x00000000fb07e140, 0x00000000fb07e200, 0x00000000fc2c0000)
No shared spaces configured.

参数值为10结果:

after all 2
[GC[DefNew: 9303K->602K(18432K), 0.0047286 secs] 9303K->8794K(38912K), 0.0047632 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] 
after all 3
[GC[DefNew: 9466K->601K(18432K), 0.0011722 secs] 17658K->8793K(38912K), 0.0011949 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
after all 4
Heap
 def new generation   total 18432K, used 9073K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,  51% used [0x00000000f8600000, 0x00000000f8e460f8, 0x00000000f9600000)
  from space 2048K,  29% used [0x00000000f9600000, 0x00000000f96966c8, 0x00000000f9800000)
  to   space 2048K,   0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9a00000)
 tenured generation   total 20480K, used 8192K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,  40% used [0x00000000f9a00000, 0x00000000fa200010, 0x00000000fa200200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2552K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  12% used [0x00000000fae00000, 0x00000000fb07e140, 0x00000000fb07e200, 0x00000000fc2c0000)
No shared spaces configured.

从结果可以看出,当-XX:MaxTenuringThreshold=1时,allocation1 对象第二次GC时进入老年代,新生代使用空间变为0;
当-XX:MaxTenuringThreshold=10时,allocation1 对象两次GC后还在新生代Survivor1(from space 2048K, 29% used);

四、动态对象年龄判断

上面说到对象达到一定的年龄将进入老年代(-XX:MaxTenuringThreshold 参数设定),但是虚拟机不一定是在只有对象达到年龄阀值才将对象放入老年代,还有动态判断规则:当Survivor区中的相同年龄对象的大小达到Survivor区空间的一半以上,也会经这个对象直接放入老年代,不会等到-XX:MaxTenuringThreshold阀值;

    /**
     * -verbose:gc -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails
     * -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:+PrintHeapAtGC
     * -XX:MaxTenuringThreshold=10
     */
    public static void testTrnuringThreshold2() 
        byte[] allocation1, allocation2, allocation3,allocation4;
        allocation1 = new byte[ _1MB / 2];
        allocation2 = new byte[ _1MB / 2];
        allocation3 = new byte[8 * _1MB];
        allocation4 = new byte[8 * _1MB];
        allocation4 = null;
        allocation4 = new byte[8 * _1MB];
    

为了更加直观的观察from Survivor 区的变化,可以加上参数-XX:+PrintHeapAtGC,每次GC前后都打印堆内存情况
从运行结果可以看到,两次GC后 Survivor区是 0k,说明allocation1、allocation2 对象并没有等到年龄阀值才计入老年代:

Heap before GC invocations=0 (full 0):
 def new generation   total 18432K, used 10199K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,  62% used [0x00000000f8600000, 0x00000000f8ff5da8, 0x00000000f9600000)
  from space 2048K,   0% used [0x00000000f9600000, 0x00000000f9600000, 0x00000000f9800000)
  to   space 2048K,   0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9a00000)
 tenured generation   total 20480K, used 0K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,   0% used [0x00000000f9a00000, 0x00000000f9a00000, 0x00000000f9a00200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2542K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb07bb80, 0x00000000fb07bc00, 0x00000000fc2c0000)
No shared spaces configured.
[GC[DefNew: 10199K->1497K(18432K), 0.0066659 secs] 10199K->9689K(38912K), 0.0066924 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap after GC invocations=1 (full 0):
 def new generation   total 18432K, used 1497K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,   0% used [0x00000000f8600000, 0x00000000f8600000, 0x00000000f9600000)
  from space 2048K,  73% used [0x00000000f9800000, 0x00000000f99765c8, 0x00000000f9a00000)
  to   space 2048K,   0% used [0x00000000f9600000, 0x00000000f9600000, 0x00000000f9800000)
 tenured generation   total 20480K, used 8192K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,  40% used [0x00000000f9a00000, 0x00000000fa200010, 0x00000000fa200200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2542K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb07bb80, 0x00000000fb07bc00, 0x00000000fc2c0000)
No shared spaces configured.

Heap before GC invocations=1 (full 0):
 def new generation   total 18432K, used 10186K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,  53% used [0x00000000f8600000, 0x00000000f8e7c410, 0x00000000f9600000)
  from space 2048K,  73% used [0x00000000f9800000, 0x00000000f99765c8, 0x00000000f9a00000)
  to   space 2048K,   0% used [0x00000000f9600000, 0x00000000f9600000, 0x00000000f9800000)
 tenured generation   total 20480K, used 8192K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,  40% used [0x00000000f9a00000, 0x00000000fa200010, 0x00000000fa200200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2545K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb07c708, 0x00000000fb07c800, 0x00000000fc2c0000)
No shared spaces configured.
[GC[DefNew: 10186K->0K(18432K), 0.0016216 secs] 18378K->9689K(38912K), 0.0016353 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap after GC invocations=2 (full 0):
 def new generation   total 18432K, used 0K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,   0% used [0x00000000f8600000, 0x00000000f8600000, 0x00000000f9600000)
  from space 2048K,   0% used [0x00000000f9600000, 0x00000000f9600100, 0x00000000f9800000)
  to   space 2048K,   0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9a00000)
 tenured generation   total 20480K, used 9688K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,  47% used [0x00000000f9a00000, 0x00000000fa376350, 0x00000000fa376400, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2545K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb07c708, 0x00000000fb07c800, 0x00000000fc2c0000)
No shared spaces configured.

Heap
 def new generation   total 18432K, used 8356K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,  51% used [0x00000000f8600000, 0x00000000f8e28fd0, 0x00000000f9600000)
  from space 2048K,   0% used [0x00000000f9600000, 0x00000000f9600100, 0x00000000f9800000)
  to   space 2048K,   0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9a00000)
 tenured generation   total 20480K, used 9688K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,  47% used [0x00000000f9a00000, 0x00000000fa376350, 0x00000000fa376400, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2552K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  12% used [0x00000000fae00000, 0x00000000fb07e268, 0x00000000fb07e400, 0x00000000fc2c0000)
No shared spaces configured.

可以试下把allocation2 对象创建注释掉运行,会发现结果Survivor区并不是为0 k,应为没有符合到规则,相同年龄对象大小占比一半以上;

五、空间分配担保

在发生Minor GC 时,如果空间不足会导致对象直接进入老年代,但是也有可能存在老年代空间也不足的情况;在每次Minor GC前,虚拟机会检查老年代的最大可用空间是否大于新生代对象大小的总和,如果是,则认为这次Minor GC 是安全的,
否则就检查是否允许担保失败(HandlerPromotionFailure 参数),如果允许,则判断老年代最大连续可用空间是否大于历次Minor GC 平均晋升老年代对象的大小,如果大于,则会尝试一次Minor GC;如果小于或者不允许,则触发一次Full GC;

取平均的方式是属于一种冒险判断,可能会出现某一次Minor GC存在大量对象存活,导致老年代担保失败,不得不进行 一次Full GC,默认情况下 HandlerPromotionFailure 参数是打开的,而且在JDK7 后,这个参数已经是无效的,虚拟机都会进行冒险担保,因为这样可以有效减少FUll GC,而且突然出现某一次Minor GC 存活大量对象的概率也比较小;

本篇主要学习GC 中内存分配的几条常见规则,并且基于Serial + Serial Old收集器进行了一些测试代码验证;

以上是关于JVM学习-java垃圾回收-内存分配的主要内容,如果未能解决你的问题,请参考以下文章

JVM内存区域管理算法-垃圾回收算法

JVM的内存分配垃圾回收策略

图解JVM垃圾内存回收算法

jvm学习笔记一(垃圾回收算法)

Java学习之垃圾回收机制

学习jvm--java内存区域