垃圾收集器
问题
HotSpot 有哪些垃圾收集器?CMS 和 G1 有什么区别?ZGC 有什么优势?如何选择垃圾收集器?
答案
垃圾收集器概览
HotSpot 提供了多种垃圾收集器,可按年代和并发能力分类:
收集器一览表
| 收集器 | 分代 | 算法 | 线程 | STW | 目标 | 参数 |
|---|---|---|---|---|---|---|
| Serial | 新生代 | 标记-复制 | 单线程 | 全程 | 简单高效 | -XX:+UseSerialGC |
| Serial Old | 老年代 | 标记-整理 | 单线程 | 全程 | 简单高效 | (同上) |
| ParNew | 新生代 | 标记-复制 | 多线程 | 全程 | 配合 CMS | -XX:+UseParNewGC |
| Parallel Scavenge | 新生代 | 标记-复制 | 多线程 | 全程 | 吞吐量 | -XX:+UseParallelGC |
| Parallel Old | 老年代 | 标记-整理 | 多线程 | 全程 | 吞吐量 | (同上) |
| CMS | 老年代 | 标记-清除 | 并发 | 部分 | 低延迟 | -XX:+UseConcMarkSweepGC |
| G1 | 整堆 | 标记-复制+整理 | 并发 | 部分 | 可控延迟 | -XX:+UseG1GC |
| ZGC | 整堆 | 标记-复制 | 并发 | 极短 | 超低延迟 | -XX:+UseZGC |
| Shenandoah | 整堆 | 标记-复制 | 并发 | 极短 | 超低延迟 | -XX:+UseShenandoahGC |
Serial / Serial Old
最基础的收集器,单线程执行,GC 期间必须 STW。
用户线程 ────────┤ STW ├──────────────
│ GC │
GC 线程 ├─────┤
- 简单高效,没有线程交互开销
- 适合客户端模式 / 小型应用(几十 MB 堆)
- Serial 是 Client 模式下的默认新生代收集器
ParNew
Serial 的多线程版本,新生代使用多个 GC 线程并行回收。
用户线程 ────────┤ STW ├──────────────
│ │
GC 线程 1 ├──────┤
GC 线程 2 ├──────┤
GC 线程 N ├──────┤
- 主要是因为它能和 CMS 配合工作
- JDK 9 后标记为废弃(G1 取代了 CMS + ParNew 的组合)
Parallel Scavenge / Parallel Old
以吞吐量优先为目标的收集器组合。
吞吐量 = 用户代码运行时间 / (用户代码运行时间 + GC 时间)
// 关键参数
-XX:+UseParallelGC // 使用 Parallel Scavenge + Parallel Old
-XX:MaxGCPauseMillis=200 // 最大 GC 停顿时间目标(毫秒)
-XX:GCTimeRatio=99 // 吞吐量目标:99 表示 GC 时间不超过 1%
-XX:+UseAdaptiveSizePolicy // 自适应策略,自动调整新生代大小、晋升阈值等
-XX:ParallelGCThreads=4 // GC 线程数
- JDK 8 默认的垃圾收集器
- 适合后台计算、批处理任务等对延迟不敏感的场景
- 自适应调节策略是其重要优势
CMS(Concurrent Mark Sweep)
以最短 STW 时间为目标的老年代收集器,使用标记-清除算法。
四个阶段:
| 阶段 | STW | 说明 |
|---|---|---|
| ① 初始标记 | 是 | 标记 GC Roots 直接关联的对象,速度快 |
| ② 并发标记 | 否 | 从 GC Roots 出发遍历对象图,与用户线程并发执行,耗时最长 |
| ③ 重新标记 | 是 | 修正并发标记期间引用变化的对象(增量更新),比初始标记稍长 |
| ④ 并发清除 | 否 | 清除不可达对象,与用户线程并发 |
CMS 的缺点:
| 问题 | 说明 |
|---|---|
| CPU 敏感 | 并发阶段占用 CPU 资源,默认 GC 线程数 = (CPU 核心数 + 3) / 4 |
| 浮动垃圾 | 并发清除阶段新产生的垃圾只能下次 GC 清理 |
| Concurrent Mode Failure | 并发期间老年代空间不足,退化为 Serial Old(长时间 STW) |
| 内存碎片 | 标记-清除算法导致碎片,大对象无法分配时触发 Full GC |
-XX:+UseConcMarkSweepGC // 启用 CMS
-XX:CMSInitiatingOccupancyFraction=75 // 老年代使用 75% 时触发 CMS
-XX:+CMSScavengeBeforeRemark // 重新标记前先做一次 Minor GC
-XX:+UseCMSCompactAtFullCollection // Full GC 时压缩整理(解决碎片)
-XX:CMSFullGCsBeforeCompaction=4 // 经过 4 次 Full GC 后压缩一次
JDK 9 标记为废弃,JDK 14 正式移除。新项目应使用 G1 或 ZGC。
G1(Garbage-First)
G1 是面向服务端的垃圾收集器,JDK 9+ 的默认收集器。它将堆划分为多个大小相等的 Region,不再有固定的新生代/老年代物理边界。
Region 类型:
| Region 类型 | 说明 |
|---|---|
| Eden | 新对象分配区 |
| Survivor | 存活对象暂存区 |
| Old | 老年代对象 |
| Humongous | 大对象(> Region 大小的 50%),可跨多个连续 Region |
| Free | 空闲 Region |
G1 的收集过程(Mixed GC):
| 阶段 | STW | 说明 |
|---|---|---|
| ① 初始标记 | 是 | 标记 GC Roots 直接关联对象,借助 Minor GC 完成 |
| ② 并发标记 | 否 | 可达性分析,使用 SATB(原始快照)处理并发修改 |
| ③ 最终标记 | 是 | 处理 SATB 记录的引用变更 |
| ④ 筛选回收 | 是 | 计算每个 Region 的回收价值,选择回收价值最高的 Region 进行回收(Garbage-First 名称由来) |
G1 的核心特性:
-XX:+UseG1GC // 启用 G1(JDK 9+ 默认)
-XX:MaxGCPauseMillis=200 // 目标最大停顿时间(默认 200ms)
-XX:G1HeapRegionSize=4m // Region 大小(1MB~32MB,必须是 2 的幂)
-XX:G1NewSizePercent=5 // 新生代最小比例
-XX:G1MaxNewSizePercent=60 // 新生代最大比例
-XX:InitiatingHeapOccupancyPercent=45 // 触发并发标记的堆占用率
-XX:G1MixedGCCountTarget=8 // 混合回收的次数目标
| 特性 | 说明 |
|---|---|
| 可预测的停顿 | -XX:MaxGCPauseMillis 设置期望停顿时间,G1 动态调整回收 Region 数量 |
| Region 化内存 | 不再有物理分代,Region 可以灵活切换角色 |
| 混合回收 | Mixed GC 同时回收新生代和部分老年代 Region |
| Remembered Set | 每个 Region 维护 RSet 记录跨 Region 引用,避免全堆扫描 |
| SATB | 使用原始快照解决并发标记的漏标问题 |
| 对比项 | CMS | G1 |
|---|---|---|
| 算法 | 标记-清除 | 标记-复制+整理 |
| 碎片 | 有碎片 | 无碎片 |
| 停顿可预测 | 不可预测 | 可预测(-XX:MaxGCPauseMillis) |
| 内存布局 | 传统分代 | Region 化 |
| 大堆表现 | 大堆时 remark 可能很长 | 大堆表现更好 |
| 适合堆大小 | < 4GB | 4GB ~ 几十 GB |
ZGC(Z Garbage Collector)
ZGC 是 JDK 11 引入的超低延迟垃圾收集器,目标是 STW 时间不超过 10ms,且不随堆大小增长。
核心技术:
| 技术 | 说明 |
|---|---|
| 染色指针(Colored Pointers) | 在指针中嵌入标记信息(64 位指针的高 4 位),无需额外内存存储标记状态 |
| 读屏障(Load Barrier) | 在读取对象引用时插入屏障代码,实现并发移动对象时的引用更新 |
| 内存多映射 | 同一块物理内存映射到多个虚拟地址(Marked0、Marked1、Remapped),配合染色指针 |
64 位指针结构(ZGC):
┌──────┬──────────┬────┬────┬────┬────┬──────────────────────────────┐
│未使用│ 未使用 │ F │ R │ M1 │ M0 │ 对象地址(44 位) │
│ 16位 │ 2位 │ 1位│ 1位│ 1位│ 1位│ 支持 16TB 堆空间 │
└──────┴──────────┴────┴────┴────┴────┴──────────────────────────────┘
│ │ │ │
│ │ │ └── Marked 0(标记位)
│ │ └─── Marked 1(标记位)
│ └──── Remapped(重映射标记)
└───── Finalizable(终结标记)
ZGC 关键特性:
-XX:+UseZGC // 启用 ZGC
-XX:+ZGenerational // JDK 21+:启用分代 ZGC(推荐)
-Xmx16g // ZGC 适合大堆
-XX:SoftMaxHeapSize=12g // 软上限,尽量不超过此大小
| 特性 | 说明 |
|---|---|
| STW < 10ms | 停顿时间与堆大小无关 |
| 支持 TB 级堆 | 最大支持 16TB |
| 并发复制 | 移动对象与用户线程并发执行 |
| 分代 ZGC(JDK 21+) | 引入分代概念,大幅提升回收效率 |
JDK 21 引入分代 ZGC(-XX:+ZGenerational),JDK 23 开始分代 ZGC 成为默认行为。分代 ZGC 将堆分为年轻代和老年代,年轻代 GC 更频繁,不需要每次都扫描整个堆,吞吐量和内存利用率显著提升。
Shenandoah
Shenandoah 由 Red Hat 开发,目标与 ZGC 类似(超低延迟),但实现方式不同:
| 对比 | ZGC | Shenandoah |
|---|---|---|
| 并发移动技术 | 染色指针 + 读屏障 | Brooks Pointer(转发指针)+ 读/写屏障 |
| 指针要求 | 64 位系统 | 不依赖指针位 |
| 分代支持 | JDK 21+(分代 ZGC) | JDK 21+ 实验性分代 |
| 厂商 | Oracle | Red Hat(OpenJDK) |
| Oracle JDK | ✅ 支持 | ❌ 不包含(仅 OpenJDK) |
垃圾收集器选择指南
| 场景 | 推荐 GC | 原因 |
|---|---|---|
| 小型应用 / 客户端 | Serial | 简单,无多线程开销 |
| 批处理 / 后台计算 | Parallel | 吞吐量优先 |
| Web 应用 / 微服务(中等堆) | G1 | 可控停顿,JDK 9+ 默认 |
| 大堆 / 超低延迟要求 | ZGC | STW < 10ms |
| OpenJDK + 超低延迟 | Shenandoah | Red Hat 方案 |
JDK 版本默认 GC:
| JDK 版本 | 默认 GC |
|---|---|
| JDK 8 | Parallel Scavenge + Parallel Old |
| JDK 9 ~ 20 | G1 |
| JDK 21+ | G1(分代 ZGC 可选) |
常见面试问题
Q1: CMS 的收集过程?有什么缺点?
答案:
四个阶段:初始标记(STW)→ 并发标记 → 重新标记(STW)→ 并发清除。其中两次 STW 都比较短暂。
缺点:
- CPU 资源敏感:并发阶段占用 CPU
- 浮动垃圾:并发清除期间新产生的垃圾本轮无法回收
- Concurrent Mode Failure:并发期间老年代满了,退化为 Serial Old
- 内存碎片:标记-清除算法导致碎片,Full GC 时才整理
Q2: G1 和 CMS 有什么区别?
答案:
| 维度 | CMS | G1 |
|---|---|---|
| 回收算法 | 标记-清除(有碎片) | 标记-复制+整理(无碎片) |
| 内存布局 | 传统连续分代 | Region 化,灵活分配 |
| 停顿预测 | 不可控 | 通过 -XX:MaxGCPauseMillis 可控 |
| 回收范围 | 只收集老年代 | Mixed GC 同时收集新老年代 |
| 处理漏标 | 增量更新 | SATB 原始快照 |
| 适合堆大小 | < 4GB | 4GB ~ 几十 GB |
| 状态 | JDK 14 移除 | JDK 9+ 默认 |
Q3: G1 为什么能做到可预测的停顿?
答案:
G1 通过以下机制实现停顿可预测:
- Region 化回收:不需要一次性回收整个老年代,而是选择回收价值最高的 Region
- 回收价值计算:维护每个 Region 的回收价值(可回收空间 / 预估回收时间),优先回收"性价比"最高的 Region
- 动态调整:根据历史 GC 数据和
-XX:MaxGCPauseMillis目标,动态调整每次回收的 Region 数量
Q4: 什么是 Remembered Set?有什么作用?
答案:
Remembered Set(记忆集)是 G1 中每个 Region 维护的一个数据结构,记录哪些 Region 中的对象引用了本 Region 中的对象。
作用:在回收某个 Region 时,只需要扫描 RSet 即可知道外部引用,不需要扫描整个堆。这是 G1 实现局部回收的关键。
代价:RSet 占用额外内存(大约堆的 10%~20%),写操作时需要维护 RSet(写屏障)。
Q5: ZGC 是如何做到超低延迟的?
答案:
ZGC 的三大核心技术:
- 染色指针:在 64 位指针的高位存储 GC 标记信息,不需要额外内存存储对象的 GC 状态
- 读屏障:在读取对象引用时检查指针颜色,如果对象已被移动则自动更正引用(自愈)
- 并发复制:对象移动与用户线程并发执行,几乎所有阶段都是并发的
STW 只在 Root 扫描阶段(固定的少量根节点),时间在亚毫秒到几毫秒。
Q6: G1 的 Young GC 和 Mixed GC 有什么区别?
答案:
| GC 类型 | 回收范围 | 触发条件 |
|---|---|---|
| Young GC | 仅所有 Eden 和 Survivor Region | Eden 满时触发 |
| Mixed GC | 所有 Young Region + 部分 Old Region | 并发标记完成后触发,选择回收价值高的 Old Region |
| Full GC | 整堆(退化,单线程) | Mixed GC 跟不上分配速度时 |
G1 尽量通过 Young GC + Mixed GC 避免 Full GC。出现 Full GC 说明需要调优。
Q7: JDK 8 默认用什么垃圾收集器?如何查看当前使用的 GC?
答案:
JDK 8 默认使用 Parallel Scavenge + Parallel Old。
查看方式:
# 方式1:命令行查看
java -XX:+PrintCommandLineFlags -version
# 方式2:运行时查看
java -XX:+PrintGCDetails -version
# 方式3:代码中查看
ManagementFactory.getGarbageCollectorMXBeans()
.forEach(gc -> System.out.println(gc.getName()));
Q8: 什么情况下应该从 G1 切换到 ZGC?
答案:
考虑切换到 ZGC 的场景:
- 堆非常大(几十 GB 以上),G1 的 Full GC 停顿时间难以接受
- 严格的延迟要求(停顿必须 < 10ms),如金融交易、实时系统
- 已使用 JDK 17+(ZGC 已成熟,推荐 JDK 21+ 使用分代 ZGC)
不建议切换的场景:
- 堆较小(< 4GB),G1 表现已足够好
- 对吞吐量要求极高(ZGC 的读屏障有一定开销)
- 使用 JDK 8/11(ZGC 不够成熟)