跳到主要内容

垃圾收集器

问题

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 时间)

ParallelGCOptions.java
// 关键参数
-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
CMSOptions.java
-XX:+UseConcMarkSweepGC                     // 启用 CMS
-XX:CMSInitiatingOccupancyFraction=75 // 老年代使用 75% 时触发 CMS
-XX:+CMSScavengeBeforeRemark // 重新标记前先做一次 Minor GC
-XX:+UseCMSCompactAtFullCollection // Full GC 时压缩整理(解决碎片)
-XX:CMSFullGCsBeforeCompaction=4 // 经过 4 次 Full GC 后压缩一次
CMS 已废弃

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 的核心特性:

G1Options.java
-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使用原始快照解决并发标记的漏标问题
G1 vs CMS
对比项CMSG1
算法标记-清除标记-复制+整理
碎片有碎片无碎片
停顿可预测不可预测可预测(-XX:MaxGCPauseMillis
内存布局传统分代Region 化
大堆表现大堆时 remark 可能很长大堆表现更好
适合堆大小< 4GB4GB ~ 几十 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 关键特性:

ZGCOptions.java
-XX:+UseZGC               // 启用 ZGC
-XX:+ZGenerational // JDK 21+:启用分代 ZGC(推荐)
-Xmx16g // ZGC 适合大堆
-XX:SoftMaxHeapSize=12g // 软上限,尽量不超过此大小
特性说明
STW < 10ms停顿时间与堆大小无关
支持 TB 级堆最大支持 16TB
并发复制移动对象与用户线程并发执行
分代 ZGC(JDK 21+)引入分代概念,大幅提升回收效率
分代 ZGC

JDK 21 引入分代 ZGC(-XX:+ZGenerational),JDK 23 开始分代 ZGC 成为默认行为。分代 ZGC 将堆分为年轻代和老年代,年轻代 GC 更频繁,不需要每次都扫描整个堆,吞吐量和内存利用率显著提升。

Shenandoah

Shenandoah 由 Red Hat 开发,目标与 ZGC 类似(超低延迟),但实现方式不同:

对比ZGCShenandoah
并发移动技术染色指针 + 读屏障Brooks Pointer(转发指针)+ 读/写屏障
指针要求64 位系统不依赖指针位
分代支持JDK 21+(分代 ZGC)JDK 21+ 实验性分代
厂商OracleRed Hat(OpenJDK)
Oracle JDK✅ 支持❌ 不包含(仅 OpenJDK)

垃圾收集器选择指南

场景推荐 GC原因
小型应用 / 客户端Serial简单,无多线程开销
批处理 / 后台计算Parallel吞吐量优先
Web 应用 / 微服务(中等堆)G1可控停顿,JDK 9+ 默认
大堆 / 超低延迟要求ZGCSTW < 10ms
OpenJDK + 超低延迟ShenandoahRed Hat 方案

JDK 版本默认 GC:

JDK 版本默认 GC
JDK 8Parallel Scavenge + Parallel Old
JDK 9 ~ 20G1
JDK 21+G1(分代 ZGC 可选)

常见面试问题

Q1: CMS 的收集过程?有什么缺点?

答案

四个阶段:初始标记(STW)→ 并发标记 → 重新标记(STW)→ 并发清除。其中两次 STW 都比较短暂。

缺点:

  1. CPU 资源敏感:并发阶段占用 CPU
  2. 浮动垃圾:并发清除期间新产生的垃圾本轮无法回收
  3. Concurrent Mode Failure:并发期间老年代满了,退化为 Serial Old
  4. 内存碎片:标记-清除算法导致碎片,Full GC 时才整理

Q2: G1 和 CMS 有什么区别?

答案

维度CMSG1
回收算法标记-清除(有碎片)标记-复制+整理(无碎片)
内存布局传统连续分代Region 化,灵活分配
停顿预测不可控通过 -XX:MaxGCPauseMillis 可控
回收范围只收集老年代Mixed GC 同时收集新老年代
处理漏标增量更新SATB 原始快照
适合堆大小< 4GB4GB ~ 几十 GB
状态JDK 14 移除JDK 9+ 默认

Q3: G1 为什么能做到可预测的停顿?

答案

G1 通过以下机制实现停顿可预测:

  1. Region 化回收:不需要一次性回收整个老年代,而是选择回收价值最高的 Region
  2. 回收价值计算:维护每个 Region 的回收价值(可回收空间 / 预估回收时间),优先回收"性价比"最高的 Region
  3. 动态调整:根据历史 GC 数据和 -XX:MaxGCPauseMillis 目标,动态调整每次回收的 Region 数量

Q4: 什么是 Remembered Set?有什么作用?

答案

Remembered Set(记忆集)是 G1 中每个 Region 维护的一个数据结构,记录哪些 Region 中的对象引用了本 Region 中的对象

作用:在回收某个 Region 时,只需要扫描 RSet 即可知道外部引用,不需要扫描整个堆。这是 G1 实现局部回收的关键。

代价:RSet 占用额外内存(大约堆的 10%~20%),写操作时需要维护 RSet(写屏障)。

Q5: ZGC 是如何做到超低延迟的?

答案

ZGC 的三大核心技术:

  1. 染色指针:在 64 位指针的高位存储 GC 标记信息,不需要额外内存存储对象的 GC 状态
  2. 读屏障:在读取对象引用时检查指针颜色,如果对象已被移动则自动更正引用(自愈)
  3. 并发复制:对象移动与用户线程并发执行,几乎所有阶段都是并发的

STW 只在 Root 扫描阶段(固定的少量根节点),时间在亚毫秒到几毫秒

Q6: G1 的 Young GC 和 Mixed GC 有什么区别?

答案

GC 类型回收范围触发条件
Young GC仅所有 Eden 和 Survivor RegionEden 满时触发
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 的场景:

  1. 堆非常大(几十 GB 以上),G1 的 Full GC 停顿时间难以接受
  2. 严格的延迟要求(停顿必须 < 10ms),如金融交易、实时系统
  3. 已使用 JDK 17+(ZGC 已成熟,推荐 JDK 21+ 使用分代 ZGC)

不建议切换的场景:

  • 堆较小(< 4GB),G1 表现已足够好
  • 对吞吐量要求极高(ZGC 的读屏障有一定开销)
  • 使用 JDK 8/11(ZGC 不够成熟)

相关链接