JVM 有哪几种常见的垃圾回收器?各自适用什么场景?
简化版
按工作区域分三类:新生代(Serial、ParNew、Parallel Scavenge)、老年代(Serial Old、Parallel Old、CMS)、面向整堆的现代收集器(G1、ZGC、Shenandoah)。选型的核心权衡是吞吐量 vs 停顿时间:Parallel 系吞吐优先,CMS/G1/ZGC 停顿优先。JDK 9 起,G1 是 HotSpot server 配置的默认收集器;受限环境和其他 JVM 实现仍要以实际选择结果为准。
详细版
- Serial / Serial Old:单线程收集,回收时完全 STW。简单、无线程切换开销,适合客户端、小内存(几百 MB)场景。
- ParNew:Serial 的多线程版本,只作用于新生代,历史上常和 CMS 搭配。
- Parallel Scavenge / Parallel Old:多线程、吞吐量优先(可控制 GC 时间占比),适合后台计算、批处理这类不在乎单次停顿、但在乎总吞吐的服务。它是 JDK 8 HotSpot server 配置的常见默认组合。
- CMS(Concurrent Mark Sweep):HotSpot 历史上具有代表性的并发老年代收集器,以缩短停顿为目标。用标记-清除,会产生内存碎片,且可能出现 Concurrent Mode Failure(并发回收赶不上分配,退化为 Serial Old 全程 STW)。JDK 9 起被标记废弃、JDK 14 移除。
- G1(Garbage First):把堆切成很多个 Region,优先回收垃圾最多的 Region,可设定目标停顿时间,兼顾吞吐与停顿。JDK 9+ HotSpot server 配置的默认收集器。
- ZGC / Shenandoah:把大量标记和对象转移工作并发化,目标是低停顿,适合延迟敏感服务;实际暂停受版本、配置、负载和硬件影响,不能承诺固定的亚毫秒数值。
完整版教学
一、先建立坐标系:GC 的两个核心指标
评价一个收集器,本质就看两件事:
- 吞吐量(Throughput):用户代码时间 /(用户代码 + GC 时间)。吞吐高 = GC 占用的比例低,单位时间干的活多。
- 停顿时间(Latency / STW):一次 GC 让业务线程「卡住」多久。停顿短 = 响应稳定、无卡顿。
这两者往往是此消彼长:想让停顿短,就得让 GC 和业务线程并发跑、多做协调工作,反而拉低总吞吐。所有收集器的设计,都是在这条线上找不同的平衡点。
二、为什么要分代 + 分新生代/老年代收集器
绝大多数对象「朝生夕灭」(弱分代假说)。于是堆分成新生代和老年代:
- 新生代对象死得快、存活少,用复制算法(把少量存活对象复制到另一块,剩下整体清空),效率高。
- 老年代对象存活多、生命长,用标记-清除 / 标记-整理。
所以经典收集器是「新生代收集器 + 老年代收集器」搭配使用,比如 ParNew + CMS、Parallel Scavenge + Parallel Old。
三、逐个理解经典收集器
Serial / Serial Old:单线程,GC 时其他线程全停。别嫌它落后——在几百 MB 的堆上,它没有多线程协调开销,停顿反而很短,是 client 模式的合理选择。
Parallel(吞吐量优先):多线程并行收集,目标是最大化吞吐。可以用 -XX:MaxGCPauseMillis(期望停顿)和 -XX:GCTimeRatio(吞吐目标)调节。适合「跑批、算数据、不在乎偶尔卡一下、只要总量快」的场景。
CMS(停顿优先的先驱):它把老年代回收拆成四个阶段——初始标记(STW)、并发标记、重新标记(STW)、并发清除,初始标记和重新标记是主要 STW 阶段,其余主要工作与业务线程并发;“短”是设计目标而非保证。代价有三:① 标记-清除留下碎片(可能大对象分配失败提前触发 Full GC);② 并发阶段占 CPU,吞吐下降;③ 并发清理时业务还在产生垃圾(浮动垃圾),若老年代被填满就 Concurrent Mode Failure,退化成单线程 Serial Old 全程 STW,反而更慢。
四、G1:现代默认收集器的思路转变
G1 不再是「一整块新生代 + 一整块老年代」,而是把堆切成上千个大小相等的 Region,每个 Region 动态扮演 Eden / Survivor / Old / Humongous(大对象) 角色。
它的核心思想是 Garbage First:维护每个 Region 的垃圾价值,回收时优先挑「垃圾最多、回收收益最高」的 Region,并在用户设定的目标停顿时间(-XX:MaxGCPauseMillis)内尽量多回收。这样既能控制停顿,又不必每次都扫全堆。
它用标记-整理 + Region 间复制,基本不产生碎片。JDK 9 起是 HotSpot server 配置的默认收集器,适合许多「中大堆 + 要求停顿可控」的服务端应用。
五、ZGC / Shenandoah:通过并发转移降低停顿
当堆大到几十上百 GB,连 G1 的停顿都嫌长。ZGC、Shenandoah 让几乎所有 GC 工作(包括并发标记、并发转移对象)都和业务线程并发,停顿时间与堆大小基本无关,目标是把停顿控制在很低水平,但不能脱离版本与负载承诺固定数值。实现机制并不相同:ZGC 使用着色指针与屏障,Shenandoah 使用转发指针和相应屏障,让业务线程在对象并发移动期间仍访问正确位置。代价是实现复杂、对吞吐略有牺牲,适合对延迟极度敏感的大堆服务。
六、怎么答选型(面试加分点)
- 小内存 / 客户端 → Serial。
- 后台计算、批处理、要吞吐 → Parallel。
- 老代码里追求低停顿 → 曾用 CMS(现在别选,已移除)。
- 中大堆、通用服务端、希望停顿可控 → 可优先评估 G1,再用真实压测验证。
- 超大堆、极致低延迟 → ZGC / Shenandoah。
七、选型必须带上 JDK 版本与业务目标
| 场景 | 可优先评估 | 需要验证的代价 |
|---|---|---|
| 批处理、吞吐优先 | Parallel GC | 单次暂停可能较长 |
| 通用服务端、可预测停顿 | G1 | 记忆集、并发标记和 Region 疏散成本 |
| 大堆、尾延迟敏感 | ZGC 或 Shenandoah | CPU、内存余量、JDK 支持与吞吐变化 |
| 单核或极小堆 | Serial GC | 无并行能力,但实现开销低 |
例如 10 秒观测窗口内业务运行 9.7 秒、GC 占 0.3 秒,吞吐量约为 97%;但若一次 300 ms 暂停击穿接口超时,平均吞吐仍不能说明体验合格。选型必须同时看吞吐、暂停 P99、分配速率、堆大小和 CPU 余量。
吞吐量 = 应用运行时间 /(应用运行时间 + GC 时间)
本例吞吐量 = 9.7 / 10 = 97%
暂停目标与吞吐目标往往相互牵制,测试时还要保留相同的预热、流量和数据规模。 JDK 9 起 HotSpot 服务端常见默认收集器是 G1,但“默认”不等于任何业务的最优解。CMS 已在 JDK 14 移除,新项目不应选用;ZGC、Shenandoah 的能力和分代模式也随 JDK 版本演进;例如 JDK 24 已移除非分代 ZGC,只保留分代模式,使用参数前必须核对目标运行时。
八、常见误区与追问
- 误区:G1 设置了
MaxGCPauseMillis就一定不会超时。 它是软目标,收集器会努力满足但不提供硬实时保证。 - 误区:ZGC 和 Shenandoah 在任何负载下都固定亚毫秒暂停。 它们以低停顿为目标,实际结果仍受版本、硬件和工作集影响。
- 误区:JDK 默认收集器就是业务最优收集器。 默认值面向通用场景,仍需用生产式负载验证。
- 追问:Parallel GC 为什么吞吐高? 它把更多工作集中在并行 STW 阶段,减少并发屏障和业务争抢,但接受更长暂停。
- 追问:G1 为什么叫 Garbage First? 它会依据回收收益等信息优先选择垃圾比例较高的候选 Region,但仍受暂停预算约束。
- 追问:并发收集器为什么仍然有 STW? 根处理、重定位同步或其他阶段仍需建立全局一致状态,只是尽量缩短暂停工作量。
- 追问:CMS 和 G1 的碎片差异是什么? CMS 主要标记清除,容易留下不连续空闲块;G1 通过 Region 间疏散获得整理效果。
面试选型结论应是“目标—候选—代价—验证指标”,而不是只报一个收集器名称。
九、加强记忆
可以用“Serial 省协调、Parallel 拼吞吐、G1 管 Region、ZGC/Shenandoah 压尾延迟”建立选型地图,CMS 则作为已移除的历史并发方案理解。任何收集器都没有免费午餐:并发阶段会占 CPU 和内存带宽,STW 并行阶段则用更集中暂停换吞吐。G1 的暂停目标以及低延迟收集器的延迟表现都不是硬实时承诺,必须绑定 JDK 版本、堆规模与生产式负载验证。最终回答落到吞吐量、暂停分位数、分配速率、回收后基线和资源余量,才是完整选型。