JVM 垃圾回收器深度解析:Parallel、CMS 与 G1
从吞吐量到延迟,从整堆回收到分区增量——看懂这三款主流 GC 的设计哲学与选型之道
背景
Java 开发者迟早会面对一个经典问题:我的应用应该用哪种垃圾回收器? 在 JDK 8 时代,Parallel GC 和 CMS 是两大主力;进入 JDK 9+,G1 成为了默认选择。 不同回收器在吞吐量与响应时间之间做出了截然不同的权衡,理解它们的内部机制,是写出高性能 Java 服务的基础。
目的
本文旨在:
- 理清 Parallel GC、CMS、G1 三者的核心设计原理
- 对比它们的基础概念(内存布局、GC 阶段、停顿模式)
- 归纳各自的适用场景与版本演进
- 以 问-答 形式提炼关键知识点,便于理解与回顾
目录
- 4.1 Region —— 物理分区,逻辑角色
- 4.2 期望时间 —— 给 GC 设一个 DDL
- 4.3 RSet 与写屏障 —— 跨 Region 引用的精确记录
- 4.4 大对象(Humongous)—— 超过 50% Region 的特殊处理
1. 吞吐量与响应时间:GC 调优的本质

- 吞吐量 = 业务运行时间 / (业务运行时间 + GC 总时间) 追求高吞吐意味着希望 GC 花费的时间占比尽可能小。
- 响应时间 = 单次 GC 停顿的时长 追求低延迟意味着每次 GC 暂停必须很短(例如 < 100ms)。
为何二者不能兼得? GC 停顿时长与频率成反比:
- 要缩短单次停顿,GC 必须更频繁地工作(并发、增量),这会引入额外的管理开销,降低吞吐量。
- 要最大化吞吐量,GC 必须尽可能减少 GC 次数,但这导致每次 GC 需要回收更多内存,单次停顿时间变长。
调优的本质:根据业务场景,在可接受的响应时间下,尽量提高吞吐量,或反之。
2. Parallel GC:吞吐量优先的整堆回收器
- 别名:吞吐量优先收集器
- 核心设计:多线程 + Stop-The-World(STW),整堆回收
- 内存布局:连续的 Eden、Survivor(S0/S1)、Old 区
- GC 阶段:
- Young GC:Eden → Survivor(复制,STW)
- Full GC:整堆标记-压缩(STW,多线程)
- 调优参数:-XX:+UseParallelGC(JDK 8 默认),-Xmn,-XX:SurvivorRatio 等
- 优点:GC 总耗时最少,吞吐量最高
- 缺点:单次停顿不可控,大堆下可能 STW 数秒
- 适用场景:批处理、离线计算、报表任务(可容忍长暂停)
3. CMS:并发标记清除的先行者
- 别名:并发标记清除收集器
- 核心设计:标记-清除算法,大部分阶段与用户线程并发执行
- 内存布局:连续整块(与 Parallel 类似)
- GC 阶段:
- 初始标记(STW,很短)
- 并发标记(与用户线程并行)
- 重新标记(STW)
- 并发清除(与用户线程并行)
- 致命缺陷:
- 内存碎片:不整理,最终导致大对象无法分配 → Full GC(Serial,极慢)
- 浮动垃圾:并发清除时新产生的垃圾只能下次处理
- Concurrent Mode Failure:并发阶段 Old 区满,降级为 Serial Full GC
- 状态:JDK 9 标记废弃,JDK 14 彻底移除
- 适用场景:JDK 8 及更早版本中,对延迟有一定要求且堆不太大的在线服务(现已不推荐)
4. G1:分区增量回收的集大成者
G1(Garbage-First)从 JDK 7 引入,JDK 9 起成为默认 GC。它通过分区(Region)与增量回收,实现了可预测的停顿时间。
4.1 Region —— 物理分区,逻辑角色
- 物理上:堆被分成多个大小相等的 Region(默认 1~32MB,约 2048 个)。
- 逻辑上:每个 Region 在某个时刻扮演 Eden、Survivor、Old 或 Humongous 中的一种角色,且角色可动态变化。
堆内存(8GB,2048 个 Region)
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ E │ E │ S │ O │ H │ O │ E │ O │ …
└────┴────┴────┴────┴────┴────┴────┴────┘
E = Eden, S = Survivor, O = Old, H = Humongous
- 优势:无需手动设置年轻代大小(-Xmn),G1 根据停顿目标动态调整各角色 Region 数量。
4.2 期望时间 —— 给 GC 设一个 DDL
- 参数:-XX:MaxGCPauseMillis=200(默认 200ms)
- 工作原理:G1 会根据这个目标,动态决定每次 Young GC 或 Mixed GC 回收多少个 Region。如果实际停顿超过目标,下次就回收更少的 Region(同时降低年轻代大小);如果远低于目标,则增加回收量,提高吞吐量。
- 效果:GC 停顿时间变得可预测且可控。
4.3 RSet 与写屏障 —— 跨 Region 引用的精确记录
问题:G1 只回收部分 Region,如何知道其他 Region 是否指向本 Region 的对象?
答案:RSet(Remembered Set) —— 每个 Region 维护一个哈希表,记录“哪些 Region 中的对象引用了本 Region 的对象”。
- 维护方式:通过写屏障(Write Barrier)。当执行 obj1.field = obj2 且两对象位于不同 Region 时,写屏障会在目标 Region 的 RSet 中添加一条记录。
- GC 时如何使用:回收 Region X 时,只需扫描 X 的 RSet 中所列出的那些 Region,找出根引用,无需全堆扫描。
代价:额外内存(RSet 约占 5%~10% 堆)和 CPU 开销(写屏障),导致吞吐量略低于 Parallel GC。
4.4 大对象(Humongous)—— 超过 50% Region 的特殊处理
- 定义:对象大小 > Region 大小的一半(例如 Region=2MB,对象>1MB)。
- 分配:直接分配在连续的 Humongous Region 中(不经过 Eden/Survivor)。
- 回收:只在 Mixed GC 或 Full GC 时回收,不参与 Young GC。
- 风险:大对象分配过快会快速占满 Humongous 区,触发 Full GC(JDK 8 单线程,极慢)。
- 调优:可增大 Region 大小(-XX:G1HeapRegionSize=32MB),或从代码层面避免超大对象。
5. 三款 GC 核心对比表
| 内存布局 | 连续整块(Eden/S0/S1/Old) | 连续整块 | 分区(Region,动态角色) |
| GC 阶段 | 全程 STW | 大部分并发 | 增量 STW(可控) |
| 内存整理 | 有(复制+压缩) | 无(标记清除→碎片) | 有(增量复制) |
| 跨代引用 | 卡表(Card Table) | 卡表+写屏障 | RSet+写屏障 |
| 单次停顿 | 长(不可控) | 较短(但不可控) | 短且可控(MaxGCPauseMillis) |
| 吞吐量 | 最高 | 较低 | 中等(略低于 Parallel) |
| 年轻代调参 | 需手动 -Xmn | 需手动 | 自适应(无需设置) |
| Full GC | 多线程并行 | 单线程 Serial(极慢) | JDK8 单线程 / JDK10+ 并行 |
| 适用 JDK 版本 | JDK 8 默认 | JDK 14 已移除 | JDK 9+ 默认 |
6. 版本演进与选型建议
| JDK 8 | 默认 | 可用(但废弃) | 需 -XX:+UseG1GC(8u40+ 生产可用) |
| JDK 9~10 | 可用 | 废弃(警告) | 默认(Full GC 仍单线程) |
| JDK 11 | 可用 | 已移除 | 默认,Full GC 单线程 |
| JDK 17+ | 可用 | 已移除 | 默认,Full GC 并行(JDK 10+ 已改进) |
选型建议:
- 批处理/离线任务:Parallel GC
- JDK 8 在线服务(堆 4-8GB):G1(显式启用)
- JDK 9+ 在线服务:G1(默认,无需参数)
- 超大堆(>64GB)且要求极低延迟:ZGC(JDK 11+ 实验,15+ 生产)
7. 重点问答
问:G1 中的 Region 与传统 GC 的 Eden/Survivor/Old 是什么关系? 答: Region 是物理内存格子,Eden/Survivor/Old 是逻辑角色。每个 Region 在某一时刻扮演其中一种角色,且角色可以动态变化。G1 不再需要固定的年轻代大小。
问:为什么 G1 能控制 GC 停顿时间? 答: 通过 -XX:MaxGCPauseMillis 设定期望值,G1 会动态调整每次回收的 Region 数量——目标越小,每次回收的 Region 越少,停顿越短,但 GC 频率升高。
问:什么是 RSet?为什么要用写屏障来维护它? 答: RSet 是每个 Region 的记录表,存放“哪些外部 Region 引用了本 Region 的对象”。写屏障在每次跨 Region 引用赋值时触发,负责更新目标 Region 的 RSet。这样 GC 回收时只需扫描 RSet 中的 Region,避免了全堆扫描。
问:G1 如何处理大对象?有什么风险? 答: 超过 Region 大小一半的对象称为大对象,直接分配在连续的 Humongous Region 中。它们不参与 Young GC,只在 Mixed GC 或 Full GC 时回收。如果频繁分配大对象,容易占满 Humongous 区,触发 Full GC(尤其在 JDK 8 中可能是单线程的,导致极长停顿)。
问:CMS 为什么会被移除? 答: 主要三个致命缺陷:内存碎片(最终导致 Full GC)、浮动垃圾(并发模式失败)、无法精确控制停顿。G1 在 JDK 9 之后已完全覆盖并超越了 CMS 的应用场景。
问:Parallel GC 还在什么场景下使用? 答: 离线批处理、数据统计、科学计算等对响应时间不敏感,只追求最大吞吐量的场景。Parallel GC 的 GC 总耗时占比最小。
问:JDK 8 高版本(8u40+)到底能不能用 G1? 答: 能。JDK 8u40 完成了并发类卸载,G1 已生产可用。需要显式添加 -XX:+UseG1GC,且 Full GC 仍是单线程的。如果条件允许,升级到 JDK 17 会得到更成熟的 G1(并行 Full GC)。
本文由 AI 整理,结合了 Java 社区公开资料与主流实践认知。


