「译文」Java 垃圾收集参考手册(四):Serial GC

本文最后更新于:2024年7月25日 下午

Serial GC (串行 GC)

Serial GC 对年轻代使用 mark-copy (标记 - 复制) 算法 , 对老年代使用 mark-sweep-compact (标记 - 清除 - 整理) 算法. 顾名思义,两者都是单线程的垃圾收集器,不能进行并行处理。两者都会触发全线暂停 (STW), 停止所有的应用线程。

因此这种 GC 算法不能充分利用多核 CPU。不管有多少 CPU 内核,JVM 在垃圾收集时都只能使用单个核心。

要启用此款收集器,只需要指定一个 JVM 启动参数即可,同时对年轻代和老年代生效:

1
java -XX:+UseSerialGC com.mypackages.MyExecutableClass

该选项只适合几百 MB 堆内存的 JVM, 而且是单核 CPU 时比较有用。 对于服务器端来说,因为一般是多个 CPU 内核,并不推荐使用,除非确实需要限制 JVM 所使用的资源。大多数服务器端应用部署在多核平台上,选择 Serial GC 就表示人为的限制系统资源的使用。 导致的就是资源闲置,多的 CPU 资源也不能用来降低延迟,也不能用来增加吞吐量。

下面让我们看看 Serial GC 的垃圾收集日志,并从中提取什么有用的信息。为了打开 GC 日志记录,我们使用下面的 JVM 启动参数:

1
2
-XX:+PrintGCDetails -XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps

产生的 GC 日志输出类似这样 (为了排版,已手工折行):

1
2
3
4
5
6
7
8
9
10
11
12
2015-05-26T14:45:37.987-0200:
151.126: [GC (Allocation Failure)
151.126: [DefNew: 629119K->69888K(629120K), 0.0584157 secs]
1619346K->1273247K(2027264K), 0.0585007 secs]
[Times: user=0.06 sys=0.00, real=0.06 secs]
2015-05-26T14:45:59.690-0200:
172.829: [GC (Allocation Failure)
172.829: [DefNew: 629120K->629120K(629120K), 0.0000372 secs]
172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs]
1832479K->755802K(2027264K),
[Metaspace: 6741K->6741K(1056768K)], 0.1856954 secs]
[Times: user=0.18 sys=0.00, real=0.18 secs]

此 GC 日志片段展示了在 JVM 中发生的很多事情。 实际上,在这段日志中产生了两个 GC 事件,其中一次清理的是年轻代,另一次清理的是整个堆内存。让我们先来分析前一次 GC, 其在年轻代中产生。

Minor GC (小型 GC)

以下代码片段展示了清理年轻代内存的 GC 事件:

2015-05-26T14:45:37.987-02001 : 151.12622 : [ GC3 (Allocation Failure4 151.126:
[DefNew5 : 629119K->69888K6 (629120K)7 , 0.0584157 secs] 1619346K->1273247K8
(2027264K)9, 0.0585007 secs10] [Times: user=0.06 sys=0.00, real=0.06 secs]11

  1. 2015-05-26T14:45:37.987-0200 – GC 事件开始的时间。其中 -0200 表示西二时区,而中国所在的东 8 区为 +0800
  2. 151.126 – GC 事件开始时,相对于 JVM 启动时的间隔时间,单位是秒。
  3. GC – 用来区分 Minor GC 还是 Full GC 的标志。GC 表明这是一次小型 GC(Minor GC)
  4. Allocation Failure – 触发 GC 的原因。本次 GC 事件,是由于年轻代中没有空间来存放新的数据结构引起的。
  5. DefNew – 垃圾收集器的名称。这个神秘的名字表示的是在年轻代中使用的:单线程,标记 - 复制 (mark-copy), 全线暂停 (STW) 垃圾收集器。
  6. 629119K->69888K – 在垃圾收集之前和之后年轻代的使用量。
  7. (629120K) – 年轻代总的空间大小。
  8. 1619346K->1273247K – 在垃圾收集之前和之后整个堆内存的使用情况。
  9. (2027264K) – 可用堆的总空间大小。
  10. 0.0585007 secs – GC 事件持续的时间,以秒为单位。
  11. [Times: user=0.06 sys=0.00, real=0.06 secs] – GC 事件的持续时间,通过三个部分来衡量:
    • user – 在此次垃圾回收过程中,所有 GC 线程所消耗的 CPU 时间之和。
    • sys – GC 过程中中操作系统调用和系统等待事件所消耗的时间。
    • real – 应用程序暂停的时间。因为串行垃圾收集器 (Serial Garbage Collector) 只使用单线程,因此 real time 等于 user 和 system 时间的总和。

可以从上面的日志片段了解到,在 GC 事件中,JVM 的内存使用情况发生了怎样的变化。此次垃圾收集之前,堆内存总的使用量为 1,619,346K。其中,年轻代使用了 629,119K。可以算出,老年代使用量为 990,227K

更重要的信息蕴含在下一批数字中,垃圾收集之后,年轻代的使用量减少了 559,231K, 但堆内存的总体使用量只下降了 346,099K。 从中可以算出,有 213,132K 的对象从年轻代提升到了老年代。

此次 GC 事件也可以用下面的示意图来说明,显示的是 GC 开始之前,以及刚刚结束之后,这两个时间点内存使用情况的快照:

04_01_serial-gc-in-young-generation.png

Full GC (完全 GC)

理解第一次 minor GC 事件后,让我们看看日志中的第二次 GC 事件:

2015-05-26T14:45:59.690-02001 : 172.8292 : [GC (Allocation Failure 172.829:
[DefNew: 629120K->629120K(629120K), 0.0000372 secs3] 172.829:[Tenured4:
1203359K->755802K5 (1398144K)6, 0.1855567 secs7 ] 1832479K->755802K8
(2027264K)9, [Metaspace: 6741K->6741K(1056768K)]10
[Times: user=0.18 sys=0.00, real=0.18 secs]11

  1. 2015-05-26T14:45:59.690-0200 – GC 事件开始的时间。其中 -0200 表示西二时区,而中国所在的东 8 区为 +0800
  2. 172.829 – GC 事件开始时,相对于 JVM 启动时的间隔时间,单位是秒。
  3. [DefNew: 629120K->629120K(629120K), 0.0000372 secs – 与上面的示例类似,因为内存分配失败,在年轻代中发生了一次 minor GC。此次 GC 同样使用的是 DefNew 收集器,让年轻代的使用量从 629120K 降为 0。注意,JVM 对此次 GC 的报告有些问题,误将年轻代认为是完全填满的。此次垃圾收集消耗了 0.0000372 秒。
  4. Tenured – 用于清理老年代空间的垃圾收集器名称。Tenured 表明使用的是单线程的全线暂停垃圾收集器,收集算法为 标记 - 清除 - 整理 (mark-sweep-compact)。
  5. 1203359K->755802K – 在垃圾收集之前和之后老年代的使用量。
  6. (1398144K) – 老年代的总空间大小。
  7. 0.1855567 secs – 清理老年代所花的时间。
  8. 1832479K->755802K – 在垃圾收集之前和之后,整个堆内存的使用情况。
  9. (2027264K) – 可用堆的总空间大小。
  10. [Metaspace: 6741K->6741K(1056768K)] – 关于 Metaspace 空间,同样的信息。可以看出,此次 GC 过程中 Metaspace 中没有收集到任何垃圾。
  11. [Times: user=0.18 sys=0.00, real=0.18 secs] – GC 事件的持续时间,通过三个部分来衡量:
    1. user – 在此次垃圾回收过程中,所有 GC 线程所消耗的 CPU 时间之和
    2. sys – GC 过程中中操作系统调用和系统等待事件所消耗的时间。
    3. real – 应用程序暂停的时间。因为串行垃圾收集器 (Serial Garbage Collector) 只使用单线程,因此 real time 等于 user 和 system 时间的总和。

和 Minor GC 相比,最明显的区别是 —— 在此次 GC 事件中,除了年轻代,还清理了老年代和 Metaspace. 在 GC 事件开始之前,以及刚刚结束之后的内存布局,可以用下面的示意图来说明:

04_02_serial-gc-in-old-gen-java.png

系列文章

「译文」Java 垃圾收集参考手册


「译文」Java 垃圾收集参考手册(四):Serial GC
https://ewhisper.cn/posts/15311/
作者
东风微鸣
发布于
2016年2月6日
许可协议