看懂 ParallelGC 的日志
在排查垃圾回收相关的问题时,一个必不可少的技能就是要能看懂 Java 的垃圾回收日志。本文将介绍打印 GC 日志相关的 JVM 参数,以及使用不同参数时 JVM 将会打印出的日志内容。
JDK 版本
1 | openjdk version "1.8.0_232" |
示例代码
在演示过程中,我们将通过在死循环中构造大对象的方式来触发垃圾回收
1 | public class GCExample { |
同时,为了让垃圾回收更早的开始,我们还需要指定一个较小的堆内存,比如我选择只分配 10MB 的空间。
1 | -Xmx10m |
GC 日志相关的 JVM 参数
与 GC 日志相关的 JVM 参数有如下 8 个:
参数名 | 参数含义 |
---|---|
-XX:+PrintGC | 输出简要 GC 日志 |
-verbose:gc | 等同于 -XX:+PrintGC |
-XX:+PrintGCDetails | 输出详细 GC 日志 |
-Xloggc:gc.log | 输出 GC 日志到文件 gc.log |
-XX:+PrintGCTimeStamps | 输出 GC 的时间戳(以 JVM 启动到当期的总时长的时间戳形式) |
-XX:+PrintGCDateStamps | 输出 GC 的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800) |
-XX:+PrintHeapAtGC | 在进行 GC 的前后打印出堆的信息 |
-XX:+PrintReferenceGC | 打印年轻代各个引用的数量以及时长 |
-XX:+PrintGC
与 -verbose:gc
虽然上面说这两个参数是等价的,但是,-verbose:gc
是一个标准的参数,而 -XX:+PrintGC
则在 JDK 9 之后被标为废弃的 (deprecated),所以建议使用 -verbose:gc
代替 -XX:+PrintGC
。
打开 -verbose:gc
使用这个参数将会打开简略的 GC 日志。打开这个参数并运行程序,很快就可以看到这样的 GC 日志:
1 | [GC (Allocation Failure) 2520K->472K(9728K), 0.0005722 secs] |
我们将上面的日志拆分开来:
GC
代表这次垃圾回收的类型。GC
代表发生了Minor GC
,Full GC
代表发生了Full GC
Allocation Failure
简述了发生 GC 的原因。在这个例子中,是因为在年轻代中没有合适的空间导致新的对象分配失败,从而发生 GC2520K->472K
说明了年轻代的占用量变化。在这个例子中,年轻代的使用量从 2520KB 减少到了 472KB9728K
表示整个堆的大小0.0005722 secs
表示了本次 GC 消耗的时间
打开 -XX:+PrintGCDetails
使用这个参数后,将会打印详细的 GC 日志,它打印的内容包含了 -verbose:gc
参数打印的内容。
1 | [GC (Allocation Failure) [PSYoungGen: 2048K->0K(2560K)] 2500K->452K(9728K), 0.0003785 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
这个参数打印的日志与上一个参数打印的日志有一部分重合,这里我们关注新增的内容:
[PSYoungGen: 2048K->0K(2560K)]
表明了使用的垃圾回收器、年轻代的占用量变化,和年轻代的大小2500K->452K(9728K)
表明了整个堆的占用量变化,和整个堆的大小[Times: user=0.00 sys=0.00, real=0.00 secs]
表示本次 GC 消耗的时间user
代表垃圾回收器消耗的 CPU 时间sys
代表系统调用或等待系统事件消耗的时间real
代表应用实际停止的时间,近似于(user + sys) / 垃圾回收器使用的线程数
打开 -XX:+PrintGCTimeStamps
这个参数不影响日志的详细程度,在打开之后,日志中会出现从 JVM 启动到出现日志时的毫秒数,比如下面例子的 1.523
。
1 | 1.523: [GC (Allocation Failure) [PSYoungGen: 2141K->96K(2560K)] 4805K->2776K(9728K), 0.0004359 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
打开 -XX:+PrintGCDateStamps
这个参数也不影响日志的详细程度,在打开之后,日志头部会加上打印日志的时间和时区,比如下面例子的 2020-02-06T10:13:08.634-0800
。
1 | 2020-02-06T10:13:08.634-0800: 1.122: [GC (Allocation Failure) [PSYoungGen: 2048K->0K(2560K)] 3328K->1280K(9728K), 0.0004243 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
打开 -XX:+PrintHeapAtGC
使用这个参数后,在打印 GC 日志时,会同时打印出堆在 GC 前后的信息,即根据不同的内存区域,分别显示在垃圾回收前后的空间使用情况。同时,日志中还以 [bottom,top,end)
的格式展现出了各个内存区域的地址,其中 bottom
为这个区域的起始位置,top
为这个区域当前的顶部位置,end
为这个区域可使用的地址上限 [^1]。
此外,日志中关于 Metaspace
相关的信息可以参考注脚 4 [^4] 指向的那篇回答。
1 | {Heap before GC invocations=1 (full 0): |
打开 -XX:+PrintReferenceGC
打开这个参数后,就可以看到各种引用的数量,以及对其垃圾回收所消耗的时长。
1 | [GC (Allocation Failure) [SoftReference, 0 refs, 0.0000125 secs][WeakReference, 0 refs, 0.0000054 secs][FinalReference, 0 refs, 0.0000045 secs][PhantomReference, 0 refs, 0 refs, 0.0000222 secs][JNI Weak Reference, 0.0000037 secs][PSYoungGen: 2112K->64K(2560K)] 3488K->1456K(9728K), 0.0005389 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
[^1]: Meaning of heap memory addresses in GC logs using -XX:+PrintHeapAtGC?
[^2]: Parallel GC - Plumber
[^3]: Parallel Full GC - Plumber
[^4]: Understanding metaspace line in JVM heap printout