【JVM】三十一、揭秘高级工程师秘籍- 掌握Full GC日志分析技巧!
本文将给大家演示一下,老年代的GC是如何触发的
1、前文回顾
之前文章已经给大家分析清楚了对象是如何进入老年代的,接着我们就给大家演示一下,老年代的GC是如何触发的。
2、示例代码
public class Demo {
public static void main(String[] args) {
byte[] array1 = new byte[4 * 1024 * 1024];
array1 = null;
byte[] array2 = new byte[2 * 1024 * 1024];
byte[] array3 = new byte[2 * 1024 * 1024];
byte[] array4 = new byte[2 * 1024 * 1024];
byte[] array5 = new byte[128 * 1024];
byte[] array6 = new byte[2 * 1024 * 1024];
}
}
3、GC日志
我们需要采用如下参数来运行上述程序:
-XX:NewSize=10485760 -XX:MaxNewSize=10485760 -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=3145728 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:gc.log
在这里,最关键的一个参数是 "-XX:PretenureSizeThreshold=3145728"。这个参数的作用是设置大对象的阈值为3MB。也就是说,当对象的大小超过3MB时,它们将直接被分配到老年代中。运行之后会得到如下GC日志:
0.308: [GC (Allocation Failure) 0.308: [ParNew (promotion failed): 7260K->7970K(9216K), 0.0048975 secs]0.314: [CMS: 8194K->6836K(10240K), 0.0049920 secs] 11356K->6836K(19456K), [Metaspace: 2776K->2776K(1056768K)], 0.0106074 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
par new generation total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
concurrent mark-sweep generation total 10240K, used 6836K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 2782K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 300K, capacity 386K, committed 512K, reserved 1048576K
4、一图一文解析日志
首先我们看如下代码:
byte[] array1 = new byte[4 * 1024 * 1024];
array1 = null;
这段代码创建了一个4MB的大对象,并将其直接分配到老年代。随后,array1不再引用这个对象。此时如下图所示。

接着看下面的代码:
byte[] array2 = new byte[2 * 1024 * 1024];
byte[] array3 = new byte[2 * 1024 * 1024];
byte[] array4 = new byte[2 * 1024 * 1024];
byte[] array5 = new byte[128 * 1024];
连续分配了4个数组,其中3个是2MB的数组,1个是128KB的数组,如下图所示,全部会进入Eden区域中。

接下来,将执行以下代码:byte[] array6 = new byte[2 * 1024 * 1024];。此时,是否还能容纳一个2MB的对象?答案是不能,因为Eden区已经无法容纳这个对象了。因此,此时会直接触发一次Young GC。
我们来分析下面的GC日志:ParNew (promotion failed): 7260K->7970K(9216K), 0.0048975 secs
这行日志揭示了在垃圾回收过程中,Eden区域原本占用了约7260KB的内存空间。然而,在执行垃圾回收操作后,发现没有对象被回收,因为有几个数组仍然被变量引用,导致它们无法被回收。
在这种情况下,我们通常会将无法回收的对象晋升到老年代。但是,当前老年代中已经存在一个容量为4MB的数组。我们需要评估是否有足够的空间在老年代中容纳3个2MB的数组和1个128KB的数组。
显然,老年代的空间不足以容纳这些额外的对象,因为总容量仅为10MB,而现有对象和新晋升的对象的总容量已经超过了这个限制。
所以此时我们看gc日志:
[CMS: 8194K->6836K(10240K), 0.0049920 secs] 11356K->6836K(19456K), [Metaspace: 2776K->2776K(1056768K)], 0.0106074 secs]
在当前的操作中,我们可以看到CMS垃圾回收器执行了一次Full GC。正如我们之前所讨论的,Full GC主要针对老年代进行Old GC,同时通常会伴随着一次Young GC。此外,它还可能触发对元数据区(也称为永久代)的GC操作。
在CMS Full GC之前,已经触发过Young GC。从日志中可以看到,Young GC的执行已经完成。接下来,执行针对老年代的Old GC,日志如下:
CMS: 8194K->6836K(10240K), 0.0049920 secs
从日志中可以看出,老年代的对象占用从8MB左右降低到了6MB左右。这个过程是怎样的呢?
实际上,这个过程相对简单。在Young GC之后,系统将两个2MB大小的数组放入了老年代。这导致了老年代对象占用的增加。然后,CMS Old GC开始执行,通过回收一些不再使用的对象来减少内存占用。最终,老年代的对象占用降低到了6MB左右。如下图。

在当前情况下,我们尝试将一个2MB的数组和一个128KB的数组放入老年代。然而,由于空间限制,这是不可能的,因此会触发CMS的Full GC。
在这次全面垃圾回收过程中,系统将会回收一个4MB的数组,因为该数组已经没有被任何对象引用。如下图所示。

接着放入进去1个2MB的数组和1个128KB的数组,如下图所示。

在观察CMS垃圾回收日志时,我们可以看到:CMS: 8194K->6836K(10240K), 0.0049920 secs。这表示在回收过程中,内存使用量从8MB减少到了6MB,如上图所示。
当CMS Full GC执行完毕后,年轻代中的对象都被转移到了老年代。因此,此时如果我们尝试在年轻代分配一个2MB的数组,操作将会成功。如下图。

5、本文总结
在本文中,我们通过一个具体的例证,详细解释了什么情况下会触发老年代的垃圾收集。这种情况发生在年轻代中的存活对象数量过多,多到无法全部迁移至老年代时,从而促使CMS垃圾收集器执行全面的垃圾回收操作。