JVM 内存分配与回收策略

JVM 内存分配与回收策略

对象的内存分配,往大的方向上讲,就是在堆上分配,少数情况下也可能直接分配在老年代中,分配的规则并不是百分百固定的。其细节决定当前使用的是那种垃圾收集器组合。当然还有虚拟机中与内存相关的参数。垃圾收集器组合一般就是 Client 模式默认:Serial+Serial Old ,Server 模式默认:Parallel+Serial Old
内存分配的动作,可以按照线程划分在不同的空间中进行,即每个线程在 java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。那个线程需要分配内存,就在那个线程的 TLAB 上分配。虚拟机是否使用 TLAB,可以通过 -XX:+-UseTLAB 参数来设定。这么做的目的之一,也是为了并发创建一个对象时,保证创建对象的线程安全性。TLAB 比较小,直接在 TLAB 上分配内存的方式称为快速分配方式,而 TLAB 大小不够,导致内存被分配在 Eden 区的内存分配方式称为慢速分配方式。

一、对象有限在 Eden 区分配

对象通常在新生代的 Eden 区进行分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC、Full GC。

  • Minor GC: 指发生在新生代的垃圾收集动作,非常频繁,速度较快。
  • Major GC: 指发生在老年代的 GC,出现 Major GC,经常会伴随一次 Minor GC,同时 Minor GC 也会引起 Major GC,一般在 GC 日志中统称为 GC,不频繁。
  • Full GC: 指发生在老年代和新生代的 GC,速度很慢,需要 Stop The World。

虚拟机参数为:-verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
即 10M 新生代,10M 老年代,10M 新生代中 8M 的 Eden 区,两个 Survivor 区各 1M。

public class EdenAllocationTest {
   private static final int _1MB = 1024 * 1024;
   public static void main(String[] args) {
      byte[] allocation1 = new byte[2 * _1MB];
      byte[] allocation2 = new byte[2 * _1MB];
      byte[] allocation3 = new byte[2 * _1MB];
      byte[] allocation4 = new byte[4 * _1MB];
   }
}

Client 模式下:

[GC [DefNew: 6487K->194K(9216K), 0.0042856 secs] 6487K->6338K(19456K), 0.0043281 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4454K [0x0000000005180000, 0x0000000005b80000, 0x0000000005b80000)
  eden space 8192K,  52% used [0x0000000005180000, 0x00000000055a9018, 0x0000000005980000)
  from space 1024K,  18% used [0x0000000005a80000, 0x0000000005ab0810, 0x0000000005b80000)
  to   space 1024K,   0% used [0x0000000005980000, 0x0000000005980000, 0x0000000005a80000)
 tenured generation   total 10240K, used 6144K [0x0000000005b80000, 0x0000000006580000, 0x0000000006580000)
   the space 10240K,  60% used [0x0000000005b80000, 0x0000000006180048, 0x0000000006180200, 0x0000000006580000)
 compacting perm gen  total 21248K, used 2982K [0x0000000006580000, 0x0000000007a40000, 0x000000000b980000)
   the space 21248K,  14% used [0x0000000006580000, 0x0000000006869890, 0x0000000006869a00, 0x0000000007a40000)
No shared spaces configured.

Server 模式下:

[GC (Allocation Failure) [PSYoungGen: 6262K->792K(9216K)] 6262K->4896K(19456K), 0.0023413 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 7173K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 77% used [0x00000000ff600000,0x00000000ffc3b5c8,0x00000000ffe00000)
  from space 1024K, 77% used [0x00000000ffe00000,0x00000000ffec6030,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
 Metaspace       used 3471K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

看到在 Client 模式下,最后分配的 4M 在新生代中,先分配的 6M 在老年代中;在 Server 模式下,最后分配的 4M 在老年代中,先分配的 6M 在新生代中。说明不同的垃圾收集器组合对于对象的分配是有影响的。讲下两者差别的原因:

  • 1、Client 模式下,新生代分配了 6M,虚拟机在 GC 前有 6487K,比 6M 也就是 6144K 多,多主要是因为 TLAB 和 EdenAllocationTest 这个对象占的空间,TLAB 可以通过 -XX:+PrintTLAB 这个虚拟机参数来查看大小。OK,6M 多了,然后来了一个 4M 的,Eden + 一个 Survivor 总共就 9M 不够分配了,这时候就会触发一次 Minor GC。但是触发 Minor GC 也没用,因为 allocation1、allocation2、allocation3 三个引用还存在,另一块 1M 的 Survivor 也不够放下这 6M,那么这次 Minor GC 的效果其实是通过分配担保机制将这 6M 的内容转入老年代中。然后再来一个 4M 的,由于此时 Minor GC 之后新生代只剩下了 194K 了,够分配了,所以 4M 顺利进入新生代。
  • 2、Server 模式下,前面都一样,但是在 GC 的时候有一点区别。在 GC 前还会进行一次判断,如果要分配的内存 >=Eden 区大小的一半,那么会直接把要分配的内存放入老年代中。要分配 4M,Eden 区 8M,刚好一半,而且老年代 10M,够分配,所以 4M 就直接进入老年代去了。为了验证一下结论,我们把 3 个 2M 之后分配的 4M 改为 3M 看一下
public class EdenAllocationTest{
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args)
    {
        byte[] allocation1 = new byte[2 * _1MB];
        byte[] allocation2 = new byte[2 * _1MB];
        byte[] allocation3 = new byte[2 * _1MB];
        byte[] allocation4 = new byte[3 * _1MB];
    }
}

运行结果:

[GC [PSYoungGen: 6487K->352K(9216K)] 6487K->6496K(19456K), 0.0035661 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 352K->0K(9216K)] [PSOldGen: 6144K->6338K(10240K)] 6496K->6338K(19456K) [PSPermGen: 2941K->2941K(21248K)], 0.0035258 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 3236K [0x000000000af40000, 0x000000000b940000, 0x000000000b940000)
  eden space 8192K, 39% used [0x000000000af40000,0x000000000b269018,0x000000000b740000)
  from space 1024K, 0% used [0x000000000b740000,0x000000000b740000,0x000000000b840000)
  to   space 1024K, 0% used [0x000000000b840000,0x000000000b840000,0x000000000b940000)
 PSOldGen        total 10240K, used 6338K [0x000000000a540000, 0x000000000af40000, 0x000000000af40000)
  object space 10240K, 61% used [0x000000000a540000,0x000000000ab70858,0x000000000af40000)
 PSPermGen       total 21248K, used 2982K [0x0000000005140000, 0x0000000006600000, 0x000000000a540000)
  object space 21248K, 14% used [0x0000000005140000,0x0000000005429890,0x0000000006600000)

看到 3M 在新生代中,6M 通过分配担保机制进入老年代了。

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

需要大量连续空间的 Java 对象成为大对象,大对象的出现会导致提前出发垃圾收集以获取更大的连续空间来进行大对象的分配。虚拟机提供了:-XX:PretenureSizeThreadshold
参数来设置大对象的阈值,超过阈值的对象直接分配到老年代。
看下面的代码,虚拟机参数为:

-XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 
-XX:PretenureSizeThreshold=3145728

最后那个参数表示大于这个设置值的对象直接在老年代中分配,这样做的目的是为了避免在 Eden 区和两个 Survivor 区之间发生大量的内存复制。

public class OldTest {
   private static final int _1MB = 1024 * 1024;
   public static void main(String[] args) {
      byte[] allocation = new byte[4 * _1MB];
   }
}

Client 模式下

Heap
 def new generation   total 9216K, used 507K [0x0000000005140000, 0x0000000005b40000, 0x0000000005b40000)
  eden space 8192K,   6% used [0x0000000005140000, 0x00000000051bef28, 0x0000000005940000)
  from space 1024K,   0% used [0x0000000005940000, 0x0000000005940000, 0x0000000005a40000)
  to   space 1024K,   0% used [0x0000000005a40000, 0x0000000005a40000, 0x0000000005b40000)
 tenured generation   total 10240K, used 4096K [0x0000000005b40000, 0x0000000006540000, 0x0000000006540000)
   the space 10240K,  40% used [0x0000000005b40000, 0x0000000005f40018, 0x0000000005f40200, 0x0000000006540000)
 compacting perm gen  total 21248K, used 2972K [0x0000000006540000, 0x0000000007a00000, 0x000000000b940000)
   the space 21248K,  13% used [0x0000000006540000, 0x00000000068272a0, 0x0000000006827400, 0x0000000007a00000)
No shared spaces configured.

Server 模式下

Heap
 PSYoungGen      total 9216K, used 4603K [0x000000000afc0000, 0x000000000b9c0000, 0x000000000b9c0000)
  eden space 8192K, 56% used [0x000000000afc0000,0x000000000b43ef40,0x000000000b7c0000)
  from space 1024K, 0% used [0x000000000b8c0000,0x000000000b8c0000,0x000000000b9c0000)
  to   space 1024K, 0% used [0x000000000b7c0000,0x000000000b7c0000,0x000000000b8c0000)
 PSOldGen        total 10240K, used 0K [0x000000000a5c0000, 0x000000000afc0000, 0x000000000afc0000)
  object space 10240K, 0% used [0x000000000a5c0000,0x000000000a5c0000,0x000000000afc0000)
 PSPermGen       total 21248K, used 2972K [0x00000000051c0000, 0x0000000006680000, 0x000000000a5c0000)
  object space 21248K, 13% used [0x00000000051c0000,0x00000000054a72a0,0x0000000006680000)

看到 Client 模式下 Eden 空间几乎没有被使用,而老年代的 10MB 空间被使用了 40%,也就是 4MB 的 allocation 对象直接就分配在老年代中,这是因为 PretenureSizeThreshold 被设置为 3MB(就是 3145728,这个参数不能像 -Xmx 之类的参数一样直接写 3MB),因此超过 3MB 的对象都会直接在老年代进行分配。Server 模式下 4M 还在新生代中。产生这个差别的原因是 -XX:PretenureSizeThreshold 这个参数对 Serial+Serial Old 垃圾收集器组合有效而对 Parallel+Serial Old 垃圾收集器组合无效。

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

每个对象有一个对象年龄计数器,与前面的对象的存储布局中的 GC 分代年龄对应。对象出生在 Eden 区、经过一次 Minor GC 后仍然存活,并能够被 Survivor 容纳,设置年龄为 1,对象在 Survivor 区每次经过一次 Minor GC,年龄就加 1,当年龄达到一定程度(默认 15),就晋升到老年代,虚拟机提供了:-XX:MaxTenuringThreshold 来进行设置。

public class AllocationTest {
   private static final int _1MB = 1024 * 1024;
   /*
    *   -Xms20M -Xmx20M -Xmn10M 
        -XX:SurvivorRatio=8 
        -XX:+PrintGCDetails
        -XX:+UseSerialGC
        -XX:MaxTenuringThreshold=1
        -XX:+PrintTenuringDistribution
     * */
   public static void testTenuringThreshold() {
      byte[] allocation1, allocation2, allocation3;
      allocation1 = new byte[_1MB / 4];
      allocation2 = new byte[4 * _1MB];
      allocation3 = new byte[4 * _1MB];
      allocation3 = null;
      allocation3 = new byte[4 * _1MB];
   }
   public static void main(String[] args) {
      testTenuringThreshold();
   }
}

运行结果:

[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age   1:     790400 bytes,     790400 total
: 5174K->771K(9216K), 0.0050541 secs] 5174K->4867K(19456K), 0.0051088 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
: 4867K->0K(9216K), 0.0015279 secs] 8963K->4867K(19456K), 0.0016327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4260K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff0290e0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4867K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  47% used [0x00000000ff600000, 0x00000000ffac0d30, 0x00000000ffac0e00, 0x0000000100000000)
 Metaspace       used 2562K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

说明:发生了两次 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 区。

四、动态对象年龄判断

对象的年龄到达了 MaxTenuringThreshold 可以进入老年代,同时,如果在 survivor 区中相同年龄所有对象大小的总和大于 survivor 区的一半,年龄大于等于该年龄的对象就可以直接进入老年代。无需等到 MaxTenuringThreshold 中要求的年龄。

public class AllocationTest02 {
   private static final int _1MB = 1024 * 1024;
    
    /*
     *     -Xms20M -Xmx20M -Xmn10M 
        -XX:SurvivorRatio=8 
        -XX:+PrintGCDetails
        -XX:+UseSerialGC
        -XX:MaxTenuringThreshold=15
        -XX:+PrintTenuringDistribution
     * */

   public static void testTenuringThreshold2() {
      byte[] allocation1, allocation2, allocation3, allocation4;
      allocation1 = new byte[_1MB / 4];
      allocation2 = new byte[_1MB / 4];
      allocation3 = new byte[4 * _1MB];
      allocation4 = new byte[4 * _1MB];
      allocation4 = null;
      allocation4 = new byte[4 * _1MB];
   }

   public static void main(String[] args) {
      testTenuringThreshold2();
   }
}

运行结果:

[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:    1048576 bytes,    1048576 total
: 5758K->1024K(9216K), 0.0049451 secs] 5758K->5123K(19456K), 0.0049968 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)
: 5120K->0K(9216K), 0.0016442 secs] 9219K->5123K(19456K), 0.0016746 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 5123K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  50% used [0x00000000ff600000, 0x00000000ffb00f80, 0x00000000ffb01000, 0x0000000100000000)
 Metaspace       used 2568K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

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

五、空间分配担保

在发生 Minor GC 时,虚拟机会检查老年代连续的空闲区域是否大于新生代所有对象的总和,若成立,则说明 Minor GC 是安全的,否则,虚拟机需要查看 HandlePromotionFailure 的值,看是否运行担保失败,若允许,则虚拟机继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若大于,将尝试进行一次 Minor GC;若小于或者 HandlePromotionFailure 设置不运行冒险,那么此时将改成一次 Full GC,以上是 JDK Update 24 之前的策略,之后的策略改变了,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC,否则将进行 Full GC。
冒险是指经过一次 Minor GC 后有大量对象存活,而新生代的 survivor 区很小,放不下这些大量存活的对象,所以需要老年代进行分配担保,把 survivor 区无法容纳的对象直接进入老年代。
具体的流程图如下:

public class AllocationTest {
    private static final int _1MB = 1024 * 1024;
    /*
     *     -Xms20M -Xmx20M -Xmn10M 
        -XX:SurvivorRatio=8 
        -XX:+PrintGCDetails
        -XX:+UseSerialGC
        -XX:+HandlePromotionFailure
     * */
    
    public static void testHandlePromotion() {
        byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7,
        allocation8;
        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];
    }
    public static void main(String[] args) {
        testHandlePromotion();
    }
}

运行的结果:

[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:     528280 bytes,     528280 total
: 7294K->515K(9216K), 0.0040766 secs] 7294K->4611K(19456K), 0.0041309 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
: 6818K->0K(9216K), 0.0012444 secs] 10914K->4611K(19456K), 0.0012760 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4611K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  45% used [0x00000000ff600000, 0x00000000ffa80d58, 0x00000000ffa80e00, 0x0000000100000000)
 Metaspace       used 2568K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

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

评论

暂无

添加新评论