项目中是如何选择垃圾回收器的?为啥选择这个?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. GC 全局认知:面试官不仅仅是想知道你背了几个回收器的名字,更是想看你能不能根据业务场景(吞吐优先、延迟优先、内存大小)做出合理选型,并且说清楚为什么。

  2. 生产实战经验:这道题本质上是在问 "你有没有真正用过"。能说出 JDK 版本、堆大小、业务特征和选型理由的,说明是真干过;只会背优缺点的,面试官一眼就能看出来。

  3. 调优意识:选型只是第一步,面试官还想看你知不知道 GC 调优的基本思路——什么时候该调、调什么参数、怎么验证效果。

核心答案

先给一张总览表,不同场景怎么选,一目了然:

场景 推荐回收器 JDK 版本 核心理由
一般 Web 应用(JDK 8) CMS / ParNew + CMS JDK 8 低延迟,适合用户交互型服务
一般 Web 应用(JDK 9+) G1 JDK 9-20 JDK 9 默认回收器,兼顾吞吐和延迟
大内存 / 低延迟(JDK 11+) ZGC JDK 11+ 亚毫秒级停顿,适合金融、交易
大内存 / 低延迟(JDK 15+) ZGC(生产可用) JDK 15+ ZGC 正式生产就绪
超大堆、极低延迟(JDK 17+) ZGC JDK 17+ 支持 TB 级堆,停顿 < 1ms
后台计算 / 批处理 Parallel GC JDK 8+ 吞吐量优先,延迟不敏感

一句话选型原则:先看 JDK 版本,再看业务对延迟的容忍度,最后看堆大小。

深度解析

一、主流垃圾回收器横向对比

上图展示了垃圾回收器的演进路线。整体脉络非常清晰:

  • Parallel GC 是最早期的并行回收器,追求高吞吐量,但停顿时间不可控,适合后台批处理任务
  • CMS 是第一款以低延迟为目标的回收器,使用 "标记-清除" 算法,大大缩短了 STW 停顿,但存在内存碎片问题,已在 JDK 9 被标记为废弃,JDK 14 正式移除
  • G1 是目前的主流选择,将堆划分为多个 Region,兼顾吞吐和延迟,JDK 9 起成为默认回收器
  • ZGCShenandoah 是新一代超低延迟回收器,停顿时间控制在亚毫秒级别,JDK 15 起生产可用

演进趋势就一个方向:让停顿越来越短,同时支持越来越大的堆

二、怎么选?按场景来

场景 1:普通 Web / 微服务应用(JDK 8)

推荐:CMS(或 ParNew + CMS 组合)

# JDK 8 典型 JVM 参数
-Xms4g -Xmx4g
-XX:+UseConcMarkSweepGC    # 使用 CMS
-XX:+UseCMSCompactAtFullCollection  # Full GC 时压缩
-XX:CMSFullGCsBeforeCompaction=2    # 每 2 次 Full GC 压缩一次

为什么:JDK 8 默认是 Parallel GC,停顿时间对用户请求影响较大。CMS 的并发标记和清除大大减少了 STW 时间,适合对响应时间有要求的 Web 服务。

不过说实话,CMS 确实有不少坑:内存碎片、Concurrent Mode Failure(晋升失败会退回 Serial GC,停顿更猛)、CPU 敏感。所以如果你的堆比较大(> 6GB),建议直接升级 JDK 用 G1。

场景 2:普通 Web / 微服务应用(JDK 9+)

推荐:G1

# JDK 11+ 典型 JVM 参数
-Xms4g -Xmx4g
-XX:+UseG1GC                         # JDK 9+ 默认就是 G1,可省略
-XX:MaxGCPauseMillis=200             # 目标最大停顿 200ms
-XX:G1HeapRegionSize=8m              # Region 大小

为什么:G1 是 JDK 9+ 的默认回收器,不是没有道理的。它把堆分成等大小的 Region,通过记录每个 Region 的回收价值(垃圾占比),优先回收价值高的 Region,从而在可控的停顿时间内获得最高的回收效率。

G1 的优势:

  • 停顿可预测:通过 -XX:MaxGCPauseMillis 可以设定目标停顿时间,G1 会尽量在这个范围内完成回收
  • 无碎片:Region 级别的复制算法,天然避免内存碎片
  • 大堆友好:比起 CMS,G1 在 6GB 以上的堆表现更稳定

场景 3:金融 / 交易 / 实时竞价(低延迟敏感)

推荐:ZGC

# JDK 17+ ZGC 典型参数
-Xms8g -Xmx8g
-XX:+UseZGC                          # 启用 ZGC
-XX:SoftMaxHeapSize=6g               # 软上限,尽量不超 6G
-XX:ZCollectionInterval=0            # 自动触发 GC

为什么:ZGC 的 STW 停顿控制在 亚毫秒级(通常 0.1ms 以内),而且停顿时间不随堆大小增长。对于金融交易、实时广告竞价这种 "每毫秒都在亏钱" 的场景,ZGC 是目前最好的选择。

ZGC 的核心技术是 染色指针读屏障,在对象移动过程中通过指针的颜色标记来判断引用是否需要更新,做到了几乎完全并发的对象移动。

场景 4:大数据 / 批处理 / 离线计算

推荐:Parallel GC

# 批处理任务典型参数
-Xms8g -Xmx8g
-XX:+UseParallelGC                   # 吞吐优先
-XX:ParallelGCThreads=8              # GC 线程数

为什么:批处理任务不关心停顿时间,但非常关心整体吞吐量。Parallel GC 的 "Stop-The-World" 式回收虽然停顿长,但回收效率最高,没有并发标记的额外开销。

三、选型决策流程

上图是一个简化的选型决策流程。实际操作中按照这个思路走就行:

  • 第一步:先确定 JDK 版本,这是最大的约束条件
  • 第二步:评估业务对延迟的容忍度,这是最关键的业务指标
  • 第三步:看堆大小,堆越大越倾向于 G1 或 ZGC
  • 第四步:结合实际压测数据做最终决定

关键点在于:选型没有绝对的对错,只有 "适不适合"。脱离业务场景谈 GC 选型,都是耍流氓。

四、常见的坑

坑 1:盲目追新

不是所有项目都适合上 ZGC。如果你的服务 99% 的请求响应时间在 50ms 以内,CMS 和 G1 完全够用,没必要为了 "用新技术" 而引入额外风险。ZGC 的读屏障有 5% 左右的吞吐量损耗,某些场景下反而不如 G1。

坑 2:忽略 JDK 默认值

很多人不知道 JDK 版本的默认回收器是什么:

  • JDK 8:Parallel GC
  • JDK 9 - JDK 21:G1
  • JDK 22+:默认还是 G1,但 ZGC 越来越成熟

如果你的应用从 JDK 8 升到 JDK 11,不显式指定 GC 参数,默认就从 Parallel GC 切到了 G1。行为变了,GC 日志格式也变了,监控面板可能要重配。

坑 3:不压测就上线

GC 选型不能只看理论,必须结合实际负载压测。同样的 G1,不同的 -XX:MaxGCPauseMillis 值,效果可能天差地别。上线前一定要用生产数据(或接近生产的数据量)做压测,观察 GC 日志中的停顿时间、吞吐量、Full GC 频率等指标。

面试高频追问

  1. G1 和 CMS 的核心区别是什么?

    最大的区别是内存布局。CMS 用的是传统的分代模型(连续的年轻代和老年代),G1 把堆划分为等大小的 Region,每个 Region 可以独立作为 Eden、Survivor、Old 或 Humongous。这让 G1 可以做到 "混合回收"——不只回收年轻代,还回收部分垃圾最多的 Old Region,从而在可控停顿时间内获得最高的回收效率。另外 CMS 用 "标记-清除" 有碎片问题,G1 用 "复制" 天然无碎片。

  2. ZGC 为什么能做到亚毫秒级停顿?

    核心是两个技术:染色指针读屏障。ZGC 把引用指针上的几个 bit 作为颜色标记,用来表示对象的移动状态。当 GC 线程在移动对象时,应用线程如果访问到了正在移动的对象,会通过读屏障触发一次 "自愈" 操作,自行修正引用。整个过程只有初始标记和最终标记有极短的 STW(通常 0.1ms 以内),其余全是并发的。

  3. 你们线上怎么监控 GC 的?

    一般是 GC 日志 + 监控平台的组合。JVM 启动参数里配上 GC 日志输出(-Xlog:gc*),然后用 Filebeat 采集到 ElasticSearch,配合 Kibana/Grafana 做可视化。重点关注的指标:Young GC 频率和耗时、Full GC 频率(这个最关键,Full GC 频繁就是有问题)、GC 总耗时占比(一般控制在 5% 以内)。

常见面试变体

  • "说说你知道的垃圾回收器,各自的优缺点"
  • "G1 回收器的原理是什么?为什么能替代 CMS?"
  • "ZGC 和 G1 的区别?什么时候该用 ZGC?"
  • "线上系统频繁 Full GC 怎么排查?"

记忆口诀

选型三看:看 JDK 版本、看延迟要求、看堆大小。

回收器口诀

  • Parallel:吞吐猛,停顿长——后台批处理的好帮手
  • CMS:延迟低,碎片多——JDK 8 时代的老功臣(已退役)
  • G1:均衡选手——JDK 9+ 默认,绝大多数场景的首选
  • ZGC:停顿极低——金融交易的 "银弹"

总结

GC 选型没有银弹,核心就是 "看场景下菜碟":JDK 8 选 CMS,JDK 9+ 选 G1,对延迟极致敏感上 ZGC,批处理用 Parallel GC。但比选型更重要的是 压测验证——理论说得再好,不如一份真实的 GC 日志有说服力。面试时把选型思路、业务场景、调优经验串起来讲,面试官一听就知道你是真用过的。