跳至正文
来两杯美式
返回

垃圾收集器大阅兵:Serial/ParNew/Parallel/CMS/G1

By 来两杯美式
发布于更新于

这是「一文拿下 GC」系列第四篇。前面讲了「怎么判断该死 + 怎么回收」——这一篇讲这些理论在生产中的实际载体:HotSpot 提供的七大垃圾收集器。

一、HotSpot 垃圾收集器架构

HotSpot 垃圾收集器架构:Young + Tenured + G1 三大分区

架构说明

收集器搭配关系(图中箭头表示可用搭配):

2017 年冷知识:当年 G1 刚成为 HotSpot 长期演进方向,但生产环境真正普及要到 Java 9(2017 年 9 月发布)成为默认收集器之后。Java 8(2014 年发布)默认还是 Parallel GC。

二、新生代收集器

2.1 Serial 收集器

JDK 1.3.1 之前虚拟机新生代收集器唯一选择——单线程的收集器。

「单线程」的意义:不是说它只会使用一个 CPU 或者一条垃圾回收线程完成垃圾收集,而是它进行垃圾收集时必须暂停其他所有工作线程,直到它收集结束(Stop The World)。

优势

开启参数

-XX:+UseSerialGC -XX:+UseSerialOldGC
# 开启前面参数后,SerialOld 参数默认开启

2.2 ParNew 收集器

Serial 收集器的多线程版本

特殊地位

2.3 Parallel Scavenge 收集器

同 ParNew 一样是并行的多线程收集器,但目标不同

核心目标达到一个可控制的吞吐量(Throughput)。

吞吐量的定义CPU 运行用户代码的时间与 CPU 总消耗的时间的比值——如虚拟机执行了 100 分钟,其中 1 分钟是垃圾回收花掉的,那么吞吐量为 99%。

停顿时间就是 GC 时其他工作线程等待的时间。停顿时间越短越适合需要与用户交互的程序。

关键参数

重要警告

并不是将停顿时间设置的越小越好——停顿时间是牺牲吞吐量和新生代空间换来的。吞吐量小,只能设置更小的新生代内存,导致垃圾收集发生的更频繁,降低吞吐量(自适应)。

自适应特性:根据配置参数自动调整堆的大小

开启参数

-XX:+UseParallelGC -XX:+UseParallelOldGC
-XX:ParallelGCThreads=n    # 开启多少个垃圾回收线程

ParallelGCThreads 默认值:CPU 核心大于 8 时,开启 5/8 个;否则为 CPU 核心数。

使用场景

三、老年代收集器

3.1 Serial Old 收集器

Serial 的老年代收集器——单线程收集器,使用标记-整理算法

两大用途

3.2 Parallel Old 收集器

使用标记-整理算法JDK 1.6 开始提供

2017 年冷知识:JDK 1.6 引入 Parallel Old 是Parallel Scavenge 终于有了「般配」的老年代搭档——之前的版本里 Parallel Scavenge 只能跟 Serial Old 搭配,浪费了多核能力。

3.3 CMS 收集器

以获取最短回收停顿时间为目标的收集器——适合在 B/S 架构服务器端使用,基于标记-清除算法

3.3.1 运作过程:四阶段

  1. 初始标记(CMS initial mark)— STW
    • 标记 GC Roots 能直接关联到的对象
    • 速度很快
  2. 并发标记(CMS concurrent mark)
    • 进行 GC Roots Tracing 过程
    • 与用户线程并发执行
  3. 重新标记(CMS remark)— STW
    • 修正并发标记期间因应用程序继续运作而导致标记产生变动的那一部分对象的标记记录
    • 停顿时间稍微比初始标记长,但远比并发标记短
  4. 并发清除(CMS concurrent sweep)
    • 与用户线程并发执行

3.3.2 缺点

1. 对 CPU 资源非常敏感

其实面向并发设计的程序都对 CPU 资源比较敏感。并发阶段虽不会导致用户线程停顿,但会因占用 CPU 资源导致应用程序变慢,总吞吐量下降

CMS 默认启动回收线程数 = (CPU 数 + 3) / 4——当 CPU 数不足 4 个时,对用户程序影响很大。比如双核,要分出去一半去收集垃圾,导致用户程序执行效率突然降低 50%。

2. 无法处理浮动垃圾

CMS 并发清理阶段用户程序也在运行,自然有新垃圾产生——如果垃圾出现在标记之后,本次垃圾收集就不能及时清理,这就是浮动垃圾

3. Concurrent Mode Failure

由于垃圾收集阶段需要和用户线程并行,因此不能像其他垃圾收集器那样几乎老年代完全填满再进行收集——需要预留部分空间提供并发收集时程序运行使用。

可使用 -XX:CMSInitiatingOccupancyFraction 值修改老年代空间占用百分比(JDK 1.6 开启,此值为 92%)。

要是 CMS 运行期间预留内存无法满足程序需求,就会发生 Concurrent Mode Failure——此时启动后备预案:使用 Serial Old 重新收集老年代,这样停顿时间就可能很长。

4. 空间碎片

CMS 是基于标记-清除算法的,垃圾收集完成后会产生大量空间碎片。这样在分配大对象时,可能出现明明老年代剩余空间很大但是找不到足够的连续空间分配大对象不得不提前触发一次 FullGC。

解决方案

3.4 G1 收集器

G1(Garbage-First)是 HotSpot 的「面向服务端应用的收集器」,目标是在停顿时间可控的前提下最大化吞吐量

核心创新

2017 年冷知识:G1 引入的 region 模型是 GC 史上一次重大变革——它打破「堆必须按分代物理划分」的传统假设。后来(2018+)的 ZGC、Shenandoah 都借鉴了 region 思想。

四、七大收集器对比

收集器算法线程STW目标适用场景
SerialYoung复制单线程简单Client 桌面
ParNewYoung复制多线程与 CMS 搭配服务端 + CMS
Parallel ScavengeYoung复制多线程吞吐量优先后台计算
Serial OldOld标记-整理单线程简单CMS 备用
Parallel OldOld标记-整理多线程吞吐量优先后台计算
CMSOld标记-清除并发+STW停顿最短B/S 架构服务端
G1Both整体标记-整理+局部复制并发+STW可控停顿可控通用(服务端)

五、选择收集器

经验法则

小结

下一篇是GC 系列的最后一篇——堆内存区域全景,从 Eden/Survivor/Old 到 PermGen/Metaspace 的完整解析。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
Java GC
第 4 / 5 篇
查看系列全部文章
  1. 01.GC 调优入门:参数、工具与参考值
  2. 02.GC 怎么判断对象「该死了」:引用计数 vs 可达性分析
  3. 03.垃圾收集算法三板斧:标记-清除、复制、标记-整理
  4. 04.垃圾收集器大阅兵:Serial/ParNew/Parallel/CMS/G1
  5. 05.堆内存区域全景:Eden/Survivor/Old + PermGen/Metaspace 演进

上一篇
堆内存区域全景:Eden/Survivor/Old + PermGen/Metaspace 演进
下一篇
垃圾收集算法三板斧:标记-清除、复制、标记-整理