【JVM深度解析】第05篇:传统垃圾收集器总览

张开发
2026/6/30 5:45:25 15 分钟阅读
【JVM深度解析】第05篇:传统垃圾收集器总览
摘要本文系统梳理 HotSpot JVM 中的传统垃圾收集器体系Serial GC单线程简单稳定、ParNew GC多线程年轻代、Parallel Scavenge Parallel Old高吞吐量组合JDK 8 默认、CMSConcurrent Mark SweepJDK 9 废弃的低停顿先驱。重点剖析 CMS 的四个并发阶段、浮动垃圾与并发失败的处理机制、内存碎片的应对策略通过各收集器的工作原理图与选型决策矩阵帮助读者理解传统收集器的能力边界为后续学习 G1/ZGC 等现代收集器建立清晰的对比基础。引言在 G1 成为 JDK 9 默认收集器之前JVM 的收集器体系已经历了三代演进单线程Serial 时代→ 多线程Parallel 时代→ 并发低停顿CMS 时代。这段历史不是过时的知识而是理解现代收集器设计决策的基础。CMS 的并发标记思想直接影响了 G1Parallel GC 的高吞吐量策略在批处理场景仍是最优选择。很多生产环境仍运行着 JDK 8Parallel GC 或 CMS 依然是日常打交道的对象。一、收集器全局视图1.1 收集器关系图年轻代收集器 老年代收集器 组合关系 ───────────────────────────────────────────────────────── Serial ──────────────→ Serial Old 单线程复制 单线程标记-整理 ParNew ──────────────→ CMS 多线程复制 ┌──────────→ 并发标记-清除 │ Parallel ─────┘ Parallel Old Scavenge ─────────────→ 多线程标记-整理 多线程吞吐量优先 注意 - ParNew 是专为配合 CMS 而生的JDK 9 以后 ParNewCMS 组合废弃 - Parallel Scavenge 不能和 CMS 配合使用内部接口不兼容 - JDK 9 推荐 G1第06篇详解1.2 收集器对比总览收集器适用代线程数算法STW吞吐量停顿时间推荐场景SerialYoung1复制是低长嵌入式/客户端/小堆Serial OldOld1标记整理是低长Serial 搭档/CMS备用ParNewYoungN复制是中中CMS 的年轻代搭档Parallel ScavengeYoungN复制是最高中批处理/后台计算Parallel OldOldN标记整理是最高中Parallel Scavenge搭档CMSOldN标记清除部分并发中短低延迟在线服务二、Serial GC简单是一种力量2.1 工作原理Serial GC 是最古老的收集器设计哲学是极简单线程、Stop-The-World、先做年轻代Serial再做老年代Serial Old。Serial GC 工作时序 用户线程: ████████████████│ │████████████████ GC线程: │ ← GC线程 (单线程) → │ └──── STW 期间 ───────┘ Minor GCSerial年轻代 步骤1STW暂停所有用户线程 步骤2从 GC Roots 出发找到年轻代所有可达对象 步骤3将 Eden From-Survivor 中的存活对象复制到 To-Survivor或晋升老年代 步骤4清空 Eden From-Survivor交换 From/To 步骤5恢复用户线程 Full GCSerial Old老年代 步骤1STW 步骤2标记老年代所有存活对象 步骤3整理存活对象向一端移动 步骤4更新所有引用 步骤5恢复用户线程2.2 串行 GC 的停顿问题JVM 内存 200MB小堆Serial GC 停顿通常在几十到几百毫秒 JVM 内存 4GB大堆Serial GC 停顿可能达 秒级 ───────────────────────────────────────────────────────── 用户感知接口响应突然变慢几百ms几秒然后恢复正常 ─────────────────────────────────────────────────────────2.3 Serial GC 的适用场景尽管停顿时间长Serial GC 在以下场景依然是最优选择堆内存小300MB的应用小堆 GC 停顿可控单线程开销比多线程协调开销更低单核 CPU 或资源极度受限的环境嵌入式、低功耗设备客户端应用Swing GUI 程序等堆通常较小# 启用 Serial GC-XX:UseSerialGC三、ParNew GCSerial 的多线程版3.1 工作原理ParNew 是 Serial GC 的多线程版本年轻代使用多线程并行 GCParNew GC 工作时序与 CMS 配合 用户线程: ████████│ │████████████████ GC线程1: │ ─ GC线程1 → │ GC线程2: │ ─ GC线程2 → │ GC线程3: │ ─ GC线程3 → │ GC线程4: │ ─ GC线程4 → │ └──── STW ────┘ 并行 GC多个 GC 线程同时工作停顿时间 ≈ Serial/NN为线程数3.2 ParNew 与 Parallel Scavenge 的区别这是很多面试题中的考点对比维度ParNewParallel Scavenge算法复制复制线程数多线程多线程关注点最小停顿时间最大吞吐量老年代搭档CMS低停顿组合Parallel Old高吞吐组合自适应调节不支持支持-XX:UseAdaptiveSizePolicy使用场景低延迟在线服务配CMS高吞吐批处理ParNew 存在的核心价值就是配合 CMSJDK 9 之后 ParNew 的地位被 G1 的年轻代 GC 取代。# 启用 ParNew通常随 CMS 一起启用-XX:UseParNewGC-XX:ParallelGCThreads4# 并行GC线程数默认 CPU核数四、Parallel GCJDK 8 的默认收集器4.1 Parallel Scavenge年轻代Parallel Scavenge 的核心目标是控制 GC 时间占总运行时间的比例即最大化吞吐量而不是最小化单次停顿时间。两个关键参数# 最大停顿时间毫秒GC 会优先保证不超过此值-XX:MaxGCPauseMillis200# 吞吐量目标GC时间 / 总时间 ≤ 1/(1N)# N99 意味着 GC 时间不超过 1%-XX:GCTimeRatio99# GC 自适应策略自动调整堆分区大小-XX:UseAdaptiveSizePolicy自适应调整策略启用 -XX:UseAdaptiveSizePolicy 后 JVM 根据运行时统计数据自动调整 - Eden 和 Survivor 的大小比例 - 对象晋升老年代的年龄阈值 优点不需要手动调优年轻代参数 缺点自适应有时会产生意外调整难以预测4.2 Parallel Old老年代Parallel Old 是 Parallel Scavenge 的老年代搭档JDK 6 引入使用多线程标记-整理算法Parallel Old GC老年代 Full GC工作时序 用户线程: █████████│ │█████████ GC线程1: │ ── 标记 ──────── 整理 ──────→ │ GC线程2: │ ── 标记 ──────── 整理 ──────→ │ GC线程3: │ ── 标记 ──────── 整理 ──────→ │ GC线程4: │ ── 标记 ──────── 整理 ──────→ │ └───────── STW ───────────────┘ 多线程并行处理 - 并行标记多线程同时遍历对象图标记存活对象 - 并行整理多线程协作移动存活对象消除碎片4.3 Parallel GC 的最佳实践# 完整的 Parallel GC 配置适合批处理/后台服务-XX:UseParallelGC# 年轻代使用 Parallel Scavenge-XX:UseParallelOldGC# 老年代使用 Parallel OldJDK7默认随-UseParallelGC启用-XX:ParallelGCThreads8# 并行GC线程数-XX:MaxGCPauseMillis500# 最大停顿 500ms-XX:GCTimeRatio99# 吞吐量目标GC 时间 1%-XX:UseAdaptiveSizePolicy# 开启自适应调整适用场景离线批处理作业数据分析、ETL 等后台定时任务对响应时间不敏感、追求高吞吐量的服务JDK 8 未指定 GC 时默认就是 Parallel Scavenge Parallel Old五、CMS GC并发低停顿的先驱CMSConcurrent Mark Sweep是最复杂也最影响深远的传统收集器它第一次实现了与用户线程并发的 GC 思想。5.1 CMS 的六个工作阶段CMS GC 完整工作时序 用户线程: ████│ │██████████████████████│ │██████████████████ CMS线程: │1 │ 2 并发标记并发 │3 │ 4 并发清除并发 └──┘ └──┘ STW STW 初始标记 重新标记 (很短) (较短) 阶段 1初始标记 (Initial Mark) — STW极短 ↓ 仅标记 GC Roots 直接关联的对象不遍历引用链速度极快 阶段 2并发标记 (Concurrent Mark) — 并发最耗时 ↓ 从 GC Roots 出发遍历整个对象图标记所有可达对象 ↓ 与用户线程并发执行期间用户线程可能修改引用关系产生浮动垃圾 ↓ 使用三色标记算法 写屏障追踪引用变化 阶段 3重新标记 (Remark) — STW较短 ↓ 修正并发标记期间因用户线程运行而产生的标记变动 ↓ 使用增量更新处理新产生的引用关系 阶段 4并发清除 (Concurrent Sweep) — 并发 ↓ 清除未被标记的对象垃圾 ↓ 与用户线程并发期间产生的新垃圾只能留到下次 CMS 处理浮动垃圾 JDK 5.0_06 新增阶段5/6 阶段 5重置并发预清理 (Concurrent Preclean) 阶段 6并发可中止预清理 (Concurrent Abortable Preclean) ↓ 减少重新标记阶段的 STW 时间5.2 三色标记算法CMS 并发标记使用三色标记算法管理对象标记状态三色含义 ● 黑色Black已标记所有子引用都已扫描 → 确定存活 ◑ 灰色Gray已标记但子引用尚未全部扫描 → 待处理队列 ○ 白色White未标记 → 可能是垃圾 初始状态所有对象白色 GC_ROOT → ○A → ○B → ○C ○D → ○E ○F孤立垃圾 标记过程 步骤1GC_ROOT 指向 AA 变灰 ◑A 步骤2扫描 A 的引用A 变黑 ●AB/D 变灰 ◑B ◑D 步骤3扫描 B 的引用B 变黑 ●BC 变灰 ◑C ...最终 ●A → ●B → ●C ●D → ●E ○F ← 垃圾白色可回收 问题并发标记期间用户线程可能修改引用关系并发标记的危险——对象消失问题危险场景不处理会导致存活对象被误回收 初始状态 ●A已扫描 → ●B已扫描 ●A → ○CC 还是白色未被扫描 GC 线程扫描完 A 后用户线程执行 A.field_to_C null; // A 断开对 C 的引用 B.field_to_C C; // B 新建对 C 的引用 结果 ●A已扫描不会再扫描 ●B已扫描不会再扫描但新增了对 C 的引用 ○C仍是白色没有被扫描到但实际是存活的 C 会被误当成垃圾回收——这是 GC 中最严重的错误存活对象被回收CMS 的解决方案增量更新Incremental Update当用户线程向黑色对象插入白色对象的引用时B → C 写屏障将 B 记录为灰色下次重新标记时重新扫描 B。 // 写屏障简化示意 void reference_write_barrier(Object from, Object to) { if (isBlack(from) isWhite(to)) { DIRTY_CARD.add(from); // 标记 from 需要重新扫描 } }5.3 CMS 的三大缺陷缺陷一内存碎片CMS 使用标记-清除算法不整理内存长期运行后老年代充满碎片长期运行后的老年代示意 ┌──────────────────────────────────────────────────────────┐ │ [活][ 碎 ][ 活 ][ 碎 ][ 活 ][ 碎碎 ][ 活 ][ 碎碎碎 ] │ └──────────────────────────────────────────────────────────┘ ↑ 碎片越来越多大对象分配失败 解决方案 -XX:UseCMSCompactAtFullCollection # Full GC 时整理碎片会 STW 很长 -XX:CMSFullGCsBeforeCompactionn # 每 n 次 Full GC 后整理一次缺陷二浮动垃圾Floating Garbage并发清除阶段用户线程还在运行新产生的垃圾对象无法被这次 CMS 回收只能等下次并发清除期间 CMS 在清除上次标记的垃圾 用户线程同时产生新垃圾 ← 这些是浮动垃圾本次不处理 影响 CMS 必须预留一部分老年代空间给并发期间新分配的对象 无法等到老年代 100% 满才 GC需要提前触发 -XX:CMSInitiatingOccupancyFraction68 # 老年代使用率达68%时触发CMS默认92%太晚缺陷三并发失败Concurrent Mode Failure如果 CMS 运行期间老年代空间不足浮动垃圾预留空间不够用CMS 会被迫中断退化为 Serial Old 做 Full GC——这才是真正的灾难性停顿正常流程 CMS 并发标记/清除低停顿 并发失败流程CMF CMS 并发中 → 老年代耗尽 → 触发 Concurrent Mode Failure → 退化为 Serial Old → 全堆 STW → 停顿数秒 触发条件 1. CMSInitiatingOccupancyFraction 设置过高触发太晚空间不够 2. 对象晋升速率过快Minor GC 晋升速度 CMS 回收速度 优化策略 降低 CMSInitiatingOccupancyFraction让 CMS 更早启动 -XX:CMSInitiatingOccupancyFraction60 # 60% 时就开始 CMS -XX:UseCMSInitiatingOccupancyOnly # 仅使用设定值不自动调整5.4 CMS 的调优参数完整清单# CMS 核心参数 -XX:UseConcMarkSweepGC# 启用 CMSJDK9 废弃但仍可用-XX:UseParNewGC# 年轻代使用 ParNew配合 CMS-XX:CMSInitiatingOccupancyFraction70# 老年代使用率 70% 触发 CMS-XX:UseCMSInitiatingOccupancyOnly# 严格按 Fraction 触发不自动调整# 碎片处理 -XX:UseCMSCompactAtFullCollection# Full GC 后整理内存-XX:CMSFullGCsBeforeCompaction5# 每5次 Full GC 整理一次# 并发线程数 -XX:ConcGCThreads4# CMS 并发标记线程数默认 CPU核数/4-XX:ParallelGCThreads8# STW 阶段并行 GC 线程数# 类卸载 -XX:CMSClassUnloadingEnabled# CMS 期间允许卸载类默认开启# 增量模式i-CMS已弃用# -XX:CMSIncrementalMode # 已废弃不建议使用六、触发 Full GC 的条件总结无论使用哪种传统收集器理解 Full GC 的触发条件是性能调优的关键触发 Full GC 的条件 1. 老年代空间不足 ├── 晋升失败Minor GC 后存活对象无法放入老年代 └── 大对象分配大对象直接进老年代但空间不足 2. 元空间不足 └── -XX:MetaspaceSize 达到阈值 3. 系统调用应尽量避免 ├── System.gc()建议禁用-XX:DisableExplicitGC └── jmap -dump生产环境慎用 4. CMS 专属触发 ├── 老年代使用率 ≥ CMSInitiatingOccupancyFraction └── Concurrent Mode Failure并发失败退化为 Serial Old 5. 统计预测 └── Minor GC 的平均晋升量 老年代剩余空间 JVM 提前触发 Full GC 以避免晋升失败七、收集器选型决策7.1 选型决策流程你的业务场景是 │ ├──→ 单核/嵌入式/堆 200MB │ └──→ Serial GC Serial Old │ ├──→ 批处理/数据分析/后台任务对响应时间不敏感 │ └──→ Parallel Scavenge Parallel OldJDK8默认 │ ├──→ 在线服务JDK 8需要低停顿 500ms │ └──→ ParNew CMS │ 注意CMS 已在 JDK 9 废弃谨慎新项目采用 │ ├──→ 在线服务JDK 9 │ └──→ G1 GC见第06篇 │ └──→ 超低延迟 10msJDK 15 └──→ ZGC 或 Shenandoah见第07/08篇7.2 实际项目中的选型建议场景推荐收集器关键参数Spring Boot 微服务JDK 17G1默认-XX:MaxGCPauseMillis200Hadoop/Spark 批处理作业Parallel GC-XX:GCTimeRatio4老项目 JDK 8低延迟ParNew CMS-XX:CMSInitiatingOccupancyFraction70嵌入式/工具类程序Serial GC-XX:UseSerialGC超低延迟金融交易 JDK 15ZGC-XX:SoftMaxHeapSize28g八、总结传统垃圾收集器体系是 JVM GC 演进的基石Serial极简单线程停顿长但开销小适合小堆/嵌入式场景ParNewSerial 的多线程版专为配合 CMS 而生Parallel Scavenge Parallel Old多线程高吞吐组合JDK 8 默认批处理首选CMS首个并发 GC低停顿先驱但有碎片/浮动垃圾/并发失败三大问题JDK 9 废弃Full GC 触发条件理解各种触发路径是优化 GC 停顿的关键CMS 的并发思想没有消亡它被 G1 继承并优化——G1 同样使用并发标记但用 Region 分区彻底解决了 CMS 的碎片问题还能更精确地控制停顿时间。下一篇预告G1 GC——JDK 9 以来的默认收集器凭借 Region 分区、增量式 Mixed GC 和可预测停顿模型成为大多数生产环境的首选。第06篇将深度解析 G1 的一切。系列导航上一篇【JVM深度解析】第04篇垃圾回收算法与实现原理下一篇【JVM深度解析】第06篇G1垃圾收集器深度解析系列目录JVM深度解析系列全集参考资料《深入理解Java虚拟机第3版》第3章 — 周志明著CMS Collector — Oracle 官方文档Parallel Collector — Oracle 官方文档JEP 291: Deprecate the Concurrent Mark Sweep (CMS) Garbage CollectorGC 算法实现解析 - PlumbrCMS GC 调优最佳实践

更多文章