前提环境

笔者JDK版本如下,如果不做指定的话,64为虚拟机1.8版本默认使用的ParallelGC垃圾收集器。

$ java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)

使用-XX:+UseSerialGC选择Serial+ Serial Old的垃圾收集器组合来验证如下内存分配和回收策略。 其中显示默认使用的是。

对象优先在Eden区分配

多数情况下,对象优先在Eden区分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。 可以添加-XX:+PrintGCDetails打印内存回收日志。

实例:(空的main方法在jdk1.8上运行得到如下日志)

# 堆大小为20M,新生代为10M,打印GC详细信息
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC

日志如下:

Heap
 def new generation   total 9216K, used 1996K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  24% used [0x00000007bec00000, 0x00000007bedf3198, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 0K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,   0% used [0x00000007bf600000, 0x00000007bf600000, 0x00000007bf600200, 0x00000007c0000000)
 Metaspace       used 3012K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 330K, capacity 388K, committed 512K, reserved 1048576K

通过日志可以知道:

  • 堆里年轻代共9216K(9M=Eden+S),其中Eden为8192K(8M),2个S区为1024K(1M)。其中已使用1996K,说明虚拟机启动后,其他线程已经占用了eden区1996K,也能说明对象是优先在Eden区分配的
  • 堆里老年代共10240K(10M),已使用0K。
  • 元数据区共4496K,已使用3208K,其中类空间容量388K,已使用353K。

大对象直接进入老年代

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,避免大对象在Eden和2个Survivor区来回复制。 例子:(设置大对象阀值为3兆)

/**
 * 大对象直接进入老年代,对Serial和ParNew垃圾收集有效
 * vm参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728
 */
public static void bigObj2old(){
    byte[] a;
    a = new byte[4 * _1MB];
}

GC日志如下:

Heap
 def new generation   total 9216K, used 1996K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  24% used [0x00000007bec00000, 0x00000007bedf3198, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000)
 Metaspace       used 3199K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K

通过日志可以知道,在设置了大对象阀值后,4M的对象确实被分配到了老年代。

注意:参数XX:PretenureSizeThreshold只对Serial和ParNew收集器有效

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

虚拟机给每个对象定义了对象年龄计数器(Age,在对象头的Mark Word里),当对象到达15岁时(-XX:MaxTenuringThreshold默认值15),就会被晋升到老年代。 如果对象在Eden出生并且经过第一次Minor GC后存活且能被Survivor容纳,就会被移动到Survivor空间,则对象年龄加1。 例子:(设置年龄阀值为1,且打印新生代各个对象年龄信息)

/**
 * 长期存活对象进入老年代
 * vm参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC
 * -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
 */
public static void liveObj2old(){
    byte[] a,b,c;
    a = new byte[_1MB / 4];
    b = new byte[4 * _1MB];
    c = new byte[4 * _1MB];
}
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age   1:     685504 bytes,     685504 total
: 6187K->669K(9216K), 0.0054269 secs] 6187K->4765K(19456K), 0.0054705 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4847K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  51% used [0x00000007bec00000, 0x00000007bf014930, 0x00000007bf400000)
  from space 1024K,  65% used [0x00000007bf500000, 0x00000007bf5a75c0, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 tenured generation   total 10240K, used 4096K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  40% used [0x00000007bf600000, 0x00000007bfa00010, 0x00000007bfa00200, 0x00000007c0000000)
 Metaspace       used 3209K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 354K, capacity 388K, committed 512K, reserved 1048576K

动态对象年龄判断

Survivor空间内相同年龄所有对象大小总和大于Survivor空间的一半,年龄大于等于的对象可以直接进入老年代,无须等到默认15岁。

例子:

/**
 * vm参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC
 * -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
 */
public static void dynamicAgeObj2old(){
    byte[] a,b,c,d;
    a = new byte[_1MB / 4];
    b = new byte[_1MB / 4];
    // a + b 大于survivor空间一半
    c = new byte[4 * _1MB];
    d = new byte[4 * _1MB];
    d = null;
    d = new byte[4 * _1MB];
}

GC日志如下:

[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:     921984 bytes,     921984 total
: 6440K->900K(9216K), 0.0061563 secs] 6440K->4996K(19456K), 0.0061984 secs] [Times: user=0.00 sys=0.01, real=0.00 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age   1:       1288 bytes,       1288 total
: 5080K->1K(9216K), 0.0018378 secs] 9176K->4983K(19456K), 0.0018675 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 4235K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  51% used [0x00000007bec00000, 0x00000007bf022a48, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400508, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 4982K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  48% used [0x00000007bf600000, 0x00000007bfadda80, 0x00000007bfaddc00, 0x00000007c0000000)
 Metaspace       used 3191K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 349K, capacity 388K, committed 512K, reserved 1048576K

通过日志可以知道,survivor没有被占用,说明a和b对象直接进入了老年代,并没有等年龄到15。

空间分配担保

在GC开始前,会先判断老年代最大可连续空间是否大于新生代所有对象总和,如果大于则说明无风险并执行Minor GC;如果不大于,再判断是否开启了空间分配担保失败,如果开启了则再判断老年代最大可以连续空间是否大于历次晋升老年代对象的平均大小,若大于则冒着风险执行一次Minor GC;若不大于且不允许空间分配担保失败,则直接执行Full GC。

虚拟机使用-XX:+HandlePromotionFailure设置允许担保失败,该参数开启后虚拟机在老年代最大可用连续空间大于历次晋升老年代对象平均值情况下尝试先执行Minor GC,其目的是为了尽可能避免Full GC。

JDK 6 Update 24之 后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC。

参考资料

  • 周志明 * 《深入理解Java虚拟机》