图 1:本文封面,点明「停顿、碎片与可预测性」三个对比维度。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料(见文末来源说明),与任何公司的当前招聘无关。CMS 与 G1 的对比是 JVM 方向的经典题,但这道题有一个常被忽略的时效点:CMS 已经在 JDK 14 中被正式移除,G1 从 JDK 9 起就是默认收集器。把这个现状说在最前面,答题就已经领先了一截——官方 JEP 363 写明移除 CMS,JEP 248 写明 G1 成为默认,这两条引用放进来,比任何背诵都有说服力。
先给核心结论:CMS 和 G1 都是低停顿收集器,区别在于 G1 用 Region 化堆和可预测的停顿目标,解决了 CMS 的两个硬伤——内存碎片与并发失败。选型上,新版 JDK 里这道题已经没有悬念:G1 是大堆低延迟场景的默认答案,CMS 只该出现在历史复盘里。
第一步,CMS 的原理与流程。
CMS 是老年代收集器,目标是把「Stop The World」的停顿压到最短。它的流程分四步:初始标记(停顿,标记 GC Roots 直接关联的对象,很快)→ 并发标记(与应用线程并行,遍历整个对象图,是最耗时的一段)→ 重新标记(停顿,修正并发标记期间变化的部分)→ 并发清除(与应用线程并行,回收垃圾)。
停顿确实短了,但代价藏在两个地方。一是算法用标记-清除,不整理内存,老年代逐渐布满碎片,碎片多到放不下大对象时会触发一次 Full GC,停顿反而更长。二是并发阶段应用还在跑,会继续产生垃圾——浮动垃圾,只能留到下一次回收;更糟的是并发期间内存被占满就会并发失败,退化为串行 Full GC。
第二步,G1 的重新设计。
G1 把整堆切成一组等大的 Region,每个 Region 可以扮演 Eden、Survivor 或老年代。它的核心策略是「垃圾优先」:维护每个 Region 里垃圾的估计价值,每次回收优先挑选垃圾最多、回收收益最高的 Region 集合,而不是整个老年代。更关键的是它可以设置停顿目标(MaxGCPauseMillis),尽力把每次停顿控制在目标内,让延迟变得可预期。回收时 G1 会做整理压缩,把存活对象拷贝到空 Region,从根上避开了 CMS 的碎片问题。
第三步,对比与选型。
把两者摆在一起看:CMS 停顿短但不可预测,碎片与浮动垃圾是结构缺陷;G1 停顿可设目标、无碎片,代价是实现更复杂、小堆上优势不明显。选型话术三段论:堆小且不在意停顿,用哪个差别不大;大堆或对 P99 延迟敏感的服务,选 G1 并调停顿目标;CMS 已被移除,遇到还在用 CMS 的老系统,迁移方向就是 G1。JEP 363 的原文措辞也印证了这个方向:G1 自 JDK 6 起就是 CMS 的预期继任者,移除 CMS 时官方建议用户迁移到 G1 或其他收集器。
补一个常被问到的细节:停顿目标不是许愿,是预算。G1 会据此反推每次回收多少个 Region,目标压得太低,回收频次被迫升高,吞吐量就被拖下来。调 G1 的正确姿势是先量出业务能接受的 P99 停顿,再把目标设在略低于它的位置,用真实流量验证,而不是一上来就把参数压到极限。
图 2:CMS 与 G1 的原理对比,以及按堆大小与延迟要求的选型决策。
易错点清单。
一,不知道 CMS 已被移除,还在讨论「新项目选 CMS 还是 G1」。二,把 CMS 的并发标记说成全程无停顿——初始标记和重新标记都要停顿。三,讲不出碎片与浮动垃圾这两个 CMS 的结构性缺陷。四,以为 G1 是一次回收整个老年代,讲不出 Region 与「垃圾优先」的策略。五,选型只背结论不讲依据:堆大小、延迟要求、停顿目标,三个因素一个都不提。
回到面试现场,建议的答题节奏:先给时效结论(CMS 已移除、G1 是默认),再按「原理、优缺点、场景」三段展开对比,最后落到选型三段论。低延迟推理服务对停顿敏感,GC 选型直接写在 P99 延迟上——吞吐优先还是响应优先,这不是 JVM 的玄学,是服务化改造的日常决策。
如果面试官追问「堆再大一个数量级、停顿要求再低一个数量级,G1 之后该看谁」,你了解 ZGC 与 Shenandoah 的设计目标吗?欢迎写出你的了解。
参考资料
- JEP 248: Make G1 the Default Garbage Collector(OpenJDK 官方:JDK 9 起 G1 成为服务端默认收集器,理由是限制停顿时间通常比最大化吞吐更重要) — https://openjdk.org/jeps/248
- JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector(OpenJDK 官方:JDK 14 移除 CMS;G1 自 JDK 6 起即为 CMS 的预期继任者,建议用户迁移至 G1 或其他收集器) — https://openjdk.org/jeps/363