「译文」Java 垃圾收集参考手册(四):Serial GC
本文最后更新于:2024年7月24日 晚上
Serial GC(串行 GC)
Serial GC 对年轻代使用 mark-copy(标记 - 复制) 算法 , 对老年代使用 mark-sweep-compact(标记 - 清除 - 整理) 算法. 顾名思义, 两者都是单线程的垃圾收集器, 不能进行并行处理。两者都会触发全线暂停(STW), 停止所有的应用线程。
因此这种 GC 算法不能充分利用多核 CPU。不管有多少 CPU 内核, JVM 在垃圾收集时都只能使用单个核心。
要启用此款收集器, 只需要指定一个 JVM 启动参数即可, 同时对年轻代和老年代生效:
1 |
|
该选项只适合几百 MB 堆内存的 JVM, 而且是单核 CPU 时比较有用。 对于服务器端来说, 因为一般是多个 CPU 内核, 并不推荐使用, 除非确实需要限制 JVM 所使用的资源。大多数服务器端应用部署在多核平台上, 选择 Serial GC 就表示人为的限制系统资源的使用。 导致的就是资源闲置, 多的 CPU 资源也不能用来降低延迟, 也不能用来增加吞吐量。
下面让我们看看 Serial GC 的垃圾收集日志, 并从中提取什么有用的信息。为了打开 GC 日志记录, 我们使用下面的 JVM 启动参数:
1 |
|
产生的 GC 日志输出类似这样(为了排版, 已手工折行):
1 |
|
此 GC 日志片段展示了在 JVM 中发生的很多事情。 实际上, 在这段日志中产生了两个 GC 事件, 其中一次清理的是年轻代, 另一次清理的是整个堆内存。让我们先来分析前一次 GC, 其在年轻代中产生。
Minor GC(小型 GC)
以下代码片段展示了清理年轻代内存的 GC 事件:
2015-05-26T14:45:37.987-0200
1 :151.1262
2 : [GC
3 (Allocation Failure
4 151.126:
[DefNew
5 :629119K->69888K
6(629120K)
7 , 0.0584157 secs]1619346K->1273247K
8
(2027264K)
9,0.0585007 secs
10][Times: user=0.06 sys=0.00, real=0.06 secs]
11
2015-05-26T14:45:37.987-0200
– GC 事件开始的时间. 其中-0200
表示西二时区, 而中国所在的东 8 区为+0800
。151.126
– GC 事件开始时, 相对于 JVM 启动时的间隔时间, 单位是秒。GC
– 用来区分 Minor GC 还是 Full GC 的标志。GC
表明这是一次 小型 GC(Minor GC)Allocation Failure
– 触发 GC 的原因。本次 GC 事件, 是由于年轻代中没有空间来存放新的数据结构引起的。DefNew
– 垃圾收集器的名称。这个神秘的名字表示的是在年轻代中使用的: 单线程, 标记 - 复制(mark-copy), 全线暂停(STW) 垃圾收集器。629119K->69888K
– 在垃圾收集之前和之后年轻代的使用量。(629120K)
– 年轻代总的空间大小。1619346K->1273247K
– 在垃圾收集之前和之后整个堆内存的使用情况。(2027264K)
– 可用堆的总空间大小。0.0585007 secs
– GC 事件持续的时间, 以秒为单位。[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 开始之前, 以及刚刚结束之后, 这两个时间点内存使用情况的快照:
Full GC(完全 GC)
理解第一次 minor GC 事件后, 让我们看看日志中的第二次 GC 事件:
2015-05-26T14:45:59.690-0200
1 :172.829
2 : [GC (Allocation Failure 172.829:
[DefNew: 629120K->629120K(629120K), 0.0000372 secs
3] 172.829:[Tenured
4:
1203359K->755802K
5(1398144K)
6,0.1855567 secs
7 ]1832479K->755802K
8
(2027264K)
9,[Metaspace: 6741K->6741K(1056768K)]
10
[Times: user=0.18 sys=0.00, real=0.18 secs]
11
2015-05-26T14:45:59.690-0200
– GC 事件开始的时间. 其中-0200
表示西二时区, 而中国所在的东 8 区为+0800
。172.829
– GC 事件开始时, 相对于 JVM 启动时的间隔时间, 单位是秒。[DefNew: 629120K->629120K(629120K), 0.0000372 secs
– 与上面的示例类似, 因为内存分配失败, 在年轻代中发生了一次 minor GC。此次 GC 同样使用的是 DefNew 收集器, 让年轻代的使用量从 629120K 降为 0。注意,JVM 对此次 GC 的报告有些问题, 误将年轻代认为是完全填满的。此次垃圾收集消耗了 0.0000372 秒。Tenured
– 用于清理老年代空间的垃圾收集器名称。Tenured 表明使用的是单线程的全线暂停垃圾收集器, 收集算法为 标记 - 清除 - 整理(mark-sweep-compact)。1203359K->755802K
– 在垃圾收集之前和之后老年代的使用量。(1398144K)
– 老年代的总空间大小。0.1855567 secs
– 清理老年代所花的时间。1832479K->755802K
– 在垃圾收集之前和之后, 整个堆内存的使用情况。(2027264K)
– 可用堆的总空间大小。[Metaspace: 6741K->6741K(1056768K)]
– 关于 Metaspace 空间, 同样的信息。可以看出, 此次 GC 过程中 Metaspace 中没有收集到任何垃圾。[Times: user=0.18 sys=0.00, real=0.18 secs]
– GC 事件的持续时间, 通过三个部分来衡量:
- user – 在此次垃圾回收过程中, 所有 GC 线程所消耗的 CPU 时间之和
- sys – GC 过程中中操作系统调用和系统等待事件所消耗的时间。
- real – 应用程序暂停的时间。因为串行垃圾收集器 (Serial Garbage Collector) 只使用单线程, 因此 real time 等于 user 和 system 时间的总和。
和 Minor GC 相比, 最明显的区别是 —— 在此次 GC 事件中, 除了年轻代, 还清理了老年代和 Metaspace. 在 GC 事件开始之前, 以及刚刚结束之后的内存布局, 可以用下面的示意图来说明: