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

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

先介绍下MinorGCFullGC的概念。

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

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

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

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, eden8Msurivior1M(from,to)

[
GC (Allocation Failure)
[DefNew: 7458K->601K(9216K), 0.0042299 secs]
7458K->6745K(19456K), 0.0042714 secs
]
[Times: user=0.01 sys=0.00, real=0.00 secs]
 
Heap
 def new generation   total 9216K, used 4944K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  53% used [0x00000000fec00000, 0x00000000ff03d8a0, 0x00000000ff400000)
  from space 1024K,  58% used [0x00000000ff500000, 0x00000000ff5967c8, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
 Metaspace       used 3014K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 331K, capacity 386K, committed 512K, reserved 1048576K

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

GC结束后, eden被占用4Mallocation4),survivor空闲,老年代占用6M allocation1allocation2allocation3) 。

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新生代收集器中有效。 

Heap
 def new generation   total 9216K, used 1642K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  20% used [0x00000000fec00000, 0x00000000fed9abc0, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 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 3018K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 332K, capacity 386K, committed 512K, reserved 1048576K

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

[GC (Allocation Failure) [DefNew
Desired survivor size 1048576 bytes, new threshold 1 (max 1)
- age   1:     617104 bytes,     617104 total
: 10158K->602K(18432K), 0.0039354 secs] 10158K->8794K(38912K), 0.0039863 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 1048576 bytes, new threshold 1 (max 1)
: 8794K->0K(18432K), 0.0009718 secs] 16986K->8794K(38912K), 0.0009963 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation   total 18432K, used 8520K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
  eden space 16384K,  52% used [0x00000000fd800000, 0x00000000fe0520d8, 0x00000000fe800000)
  from space 2048K,   0% used [0x00000000fe800000, 0x00000000fe800000, 0x00000000fea00000)
  to   space 2048K,   0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000fec00000)
 tenured generation   total 20480K, used 8794K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
   the space 20480K,  42% used [0x00000000fec00000, 0x00000000ff496aa0, 0x00000000ff496c00, 0x0000000100000000)
 Metaspace       used 3017K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 332K, capacity 386K, committed 512K, reserved 1048576K

-XX:MaxTenuringThreshold=15

[GC (Allocation Failure) [DefNew
Desired survivor size 1048576 bytes, new threshold 15 (max 15)
- age   1:     616496 bytes,     616496 total
: 9831K->602K(18432K), 0.0046498 secs] 9831K->8794K(38912K), 0.0046893 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 1048576 bytes, new threshold 15 (max 15)
- age   1:       2720 bytes,       2720 total
- age   2:     612904 bytes,     615624 total
: 9121K->601K(18432K), 0.0013255 secs] 17313K->8793K(38912K), 0.0013543 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation   total 18432K, used 9612K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
  eden space 16384K,  55% used [0x00000000fd800000, 0x00000000fe0cce80, 0x00000000fe800000)
  from space 2048K,  29% used [0x00000000fe800000, 0x00000000fe8964c8, 0x00000000fea00000)
  to   space 2048K,   0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000fec00000)
 tenured generation   total 20480K, used 8192K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
   the space 20480K,  40% used [0x00000000fec00000, 0x00000000ff400010, 0x00000000ff400200, 0x0000000100000000)
 Metaspace       used 3016K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 331K, 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区。所以,最后的结果是allocation1allocation2在老年代,allocation3Eden区。

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];
}

GC 结果输出

[GC (Allocation Failure) [DefNew
Desired survivor size 1048576 bytes, new threshold 1 (max 15)
- age   1:    1682480 bytes,    1682480 total
: 11182K->1643K(18432K), 0.0042709 secs] 11182K->9835K(38912K), 0.0043135 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 1048576 bytes, new threshold 15 (max 15)
: 9835K->0K(18432K), 0.0013642 secs] 18027K->9831K(38912K), 0.0013890 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation   total 18432K, used 8356K [0x00000000fd800000, 0x00000000fec00000, 0x00000000fec00000)
  eden space 16384K,  51% used [0x00000000fd800000, 0x00000000fe0290e0, 0x00000000fe800000)
  from space 2048K,   0% used [0x00000000fe800000, 0x00000000fe800000, 0x00000000fea00000)
  to   space 2048K,   0% used [0x00000000fea00000, 0x00000000fea00000, 0x00000000fec00000)
 tenured generation   total 20480K, used 9831K [0x00000000fec00000, 0x0000000100000000, 0x0000000100000000)
   the space 20480K,  48% used [0x00000000fec00000, 0x00000000ff599e28, 0x00000000ff59a000, 0x0000000100000000)
 Metaspace       used 3185K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 386K, committed 512K, reserved 1048576K

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

5、空间分配担保

jdk1.6 update24之前的担保流程

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

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

jdk1.6 update24之后的担保流程

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

/**
 * 空间分配担保
 * 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 (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:     616528 bytes,     616528 total
: 7458K->602K(9216K), 0.0029249 secs] 7458K->4698K(19456K), 0.0029687 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age   1:      17320 bytes,      17320 total
: 7070K->16K(9216K), 0.0010989 secs] 11166K->4711K(19456K), 0.0011285 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
Heap
 def new generation   total 9216K, used 2147K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
  from space 1024K,   1% used [0x00000000ff400000, 0x00000000ff4043a8, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4694K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  45% used [0x00000000ff600000, 0x00000000ffa95a60, 0x00000000ffa95c00, 0x0000000100000000)
 Metaspace       used 3183K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 386K, committed 512K, reserved 1048576K

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