最近在看GC日志相关的东西,众所周知,当Eden区空间不足时会触发minor GC,老年代空间不足时会触发full GC,但是下面的例子中,老年代的空间并没有满,也出现了full GC,什么情况,了解一下!

例子代码:

 1package test;
 2
 3public class JVMTest2 {
 4    // java -Xms18m -Xmx18m -Xmn10m -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCCause -XX:+PrintHeapAtGC -XX:SurvivorRatio=8 test.JVMTest2
 5    private static final int MB = 1024 * 1024; // 1M
 6    private static final int KB = 1024; // 1 kb
 7
 8    public static void main(String[] args) {
 9		byte[] arr1 = new byte[2048 * KB];
10        byte[] arr2 = new byte[2048 * KB];
11		byte[] arr3 = new byte[2048 * KB];
12        byte[] arr4 = new byte[2048 * KB];
13    }	
14}

堆的内存分配为:总大小20m,新生代和老年代各10m。Eden区8m,Survivor区的S0和S1分别是1m。我加上打印GC日志相关的命令行参数运行程序:

1java -Xms22m -Xmx22m -Xmn10m -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCCause -XX:SurvivorRatio=8 test.JVMTest2

输出的结果为:

 1$ java -Xms22m -Xmx22m -Xmn10m -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCCause -XX:SurvivorRatio=8 test.JVMTest2
 2[GC (Allocation Failure) [PSYoungGen: 7129K->648K(9216K)] 7129K->6800K(21504K), 0.0034548 secs] [Times: user=0.06 sys=0.00, real=0.00 secs]
 3[Full GC (Ergonomics) [PSYoungGen: 648K->0K(9216K)] [ParOldGen: 6152K->6663K(12288K)] 6800K->6663K(21504K), [Metaspace: 2569K->2569K(1056768K)], 0.0053348 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
 4Heap
 5 PSYoungGen      total 9216K, used 2130K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
 6  eden space 8192K, 26% used [0x00000000ff600000,0x00000000ff814930,0x00000000ffe00000)
 7  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 8  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 9 ParOldGen       total 12288K, used 6663K [0x00000000fea00000, 0x00000000ff600000, 0x00000000ff600000)
10  object space 12288K, 54% used [0x00000000fea00000,0x00000000ff081d20,0x00000000ff600000)
11 Metaspace       used 2576K, capacity 4486K, committed 4864K, reserved 1056768K
12  class space    used 282K, capacity 386K, committed 512K, reserved 1048576K

从代码上看,前面3个数组加起来一共是6m,再加上JVM在Eden区自带的内存(简称“基础内存消耗”)大概1m多,所以Eden区占用的空间7m多,当new字节数组arr4时,空间不够了,自然会触发minorGC!根据空间担保机制,数组对象arr1、arr2和arr3都会直接晋升到老年代,所以执行GC后老年代应该有6m多。日志显示:

1[GC (Allocation Failure) [PSYoungGen: 7129K->648K(9216K)] 7129K->6800K(21504K), 0.0034548 secs]

执行minorGC,新生代的存活对象由7129K减少到680K,堆的总使用大小由7129K减少到6800K,因此,新生代晋升到老年代的大小为6800K-648K=6152K,基本上就是前面3个字节数组对象的总和再加上少许附加占用内存。那么问题来了:

1[Full GC (Ergonomics) [PSYoungGen: 648K->0K(9216K)] [ParOldGen: 6152K->6663K(12288K)] 6800K->6663K(21504K), [Metaspace: 2569K->2569K(1056768K)], 0.0053348 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

这行日志显示执行了fullGC,为什么会执行fullGC呢,老年代的空间明明还没有满,还剩下3900K左右!Full GC日志中的Ergonomics又表示什么意思呢,带着这些疑问我开始Google相关的资料。

Ergonomics的字面意思是人体工程学,它是产生Full GC的原因之一,GC Ergonomics是JVM GC的一种自适应自动化调优策略,JVM根据新生代晋升到老年代的历史数据推测老年代空间,将会不足以容纳下一次GC时由新生代晋升过来的对象总大小,所以提前触发Full GC。那么GC Ergonomics的算法是什么,有什么规律吗?

接着上面的例子,我把老年代的空间增大2m,结果没有变化,我再增加2m,也就是14m,结果发生了变化:

 1 java -Xms24m -Xmx24m -Xmn10m -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCCause -XX:SurvivorRatio=8 test.JVMTest2
 2[GC (Allocation Failure) [PSYoungGen: 7129K->616K(9216K)] 7129K->6768K(23552K), 0.0035058 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
 3Heap
 4 PSYoungGen      total 9216K, used 2746K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
 5  eden space 8192K, 26% used [0x00000000ff600000,0x00000000ff814930,0x00000000ffe00000)
 6  from space 1024K, 60% used [0x00000000ffe00000,0x00000000ffe9a020,0x00000000fff00000)
 7  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 8 ParOldGen       total 14336K, used 6152K [0x00000000fe800000, 0x00000000ff600000, 0x00000000ff600000)
 9  object space 14336K, 42% used [0x00000000fe800000,0x00000000fee02030,0x00000000ff600000)
10 Metaspace       used 2576K, capacity 4486K, committed 4864K, reserved 1056768K
11  class space    used 282K, capacity 386K, committed 512K, reserved 1048576K

于是我粗浅的认为,当前一次新生代晋升到老年代的对象大小大于老年代的剩余空间时,Full GC (Ergonomics)发生了,对应起来就是,新生代晋升大小6152K < 老年代的剩余空间8284K

距离真相甚远,我认真参考了这3篇文章:

总结起来就是:

  1. 如果晋升到老生代的平均大小大于老生代的剩余大小,则会返回true,认为需要一次full gc
  2. 目前新生代已经使用的大小和之前晋升到老年代的平均大小,如果这2个值中的任意一个值都比当前老年代的剩余空间还大,直接执行full GC,否则执行minor GC。
  3. 在YGC执行后,平均晋升到老年代的大小 > 老年代剩余空间大小 ? 触发Full GC : 什么都不做。

针对这3个原因,我查阅了大量资料,也做过了大量的测试模拟,第一条是正确的,但是第二条和第三条经过测试很容易被推翻掉。

根据这几天反复的实验大概可以认为,当Eden区空间不足而触发minor GC时,JVM会去看老年代剩余空间大小,如果小于之前由新生代晋升到老年代的大小,则会先触发一次minor GC,紧跟着一次Full GC(Ergonomics),如果老年代的剩余空间明显过小时,则直接执行Full GC(Ergonomics),根本不去执行minor GC了。如果老年代的剩余空间接近满了或已经满了,Full GC的原因不再是Ergonomics,而是Allocation Failure。

关于GC Ergonomics,就研究到这里,有空再继续琢磨,看能不能得到更精确的结论。