项目中是如何选择垃圾回收器的?为啥选择这个?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
考察对主流垃圾回收器的理解:面试官想确认你是否熟悉 JVM 中常见的垃圾回收器(Serial、Parallel、CMS、G1、ZGC 等),以及它们各自的核心特点和工作机制。
-
考察根据业务场景做技术选型的能力:除了知道概念,更重要的是能否根据项目实际(如吞吐量要求、延迟敏感度、堆内存大小、硬件资源)做出合理的选择,并说出背后的权衡。
-
考察实战经验和调优意识:你是否在项目中真正使用过这些回收器?有没有通过监控 GC 日志、调整参数来优化过?选型后如何验证效果?
-
考察对 JVM 发展趋势的关注:随着 JDK 版本的演进,一些旧的回收器(如 CMS)被废弃,新的低延迟回收器(ZGC、Shenandoah)出现。面试官希望看到你关注技术更新,并有前瞻性的思考。
核心答案
在项目中选择 JVM 垃圾回收器,没有 “最好” 的,只有 “最适合” 的。选择的核心依据是应用程序的性能目标 —— 通常在这几个维度之间权衡:吞吐量(CPU 用于业务代码的时间比例)、暂停时间(GC 导致应用停顿的时间)、内存占用以及实时性。
- 如果应用是后台批处理、科学计算或离线分析:这类场景对吞吐量要求极高,对暂停时间不太敏感。我会选择 Parallel Scavenge(新生代)+ Parallel Old(老年代) 组合,它是 JDK 8 默认的并行回收器,能在多核 CPU 下最大化吞吐量。
- 如果是 Web 服务、微服务等在线业务系统:这类系统要求低延迟,停顿时间必须可控。在 JDK 8 且堆内存不大(<6GB)时,可能考虑 CMS(但 CMS 已废弃,且容易产生碎片);现在更通用的选择是 G1(Garbage-First),它是 JDK 9 及以后的默认回收器,能通过设定预期的最大停顿时间(
-XX:MaxGCPauseMillis)来平衡吞吐量与延迟,特别适合大堆内存(6GB+)的场景。 - 如果是对延迟极端苛刻的系统:比如高频交易、实时推荐,要求停顿时间在 10 毫秒甚至 1 毫秒以内。此时我会选用 ZGC(JDK 11 引入,JDK 15 正式生产可用)或 Shenandoah(JDK 12 引入,由 RedHat 主导)。它们几乎能做到并发收集,暂停时间与堆大小无关。
- 如果是单机、内存很小(<100MB)的客户端应用:直接用 Serial 回收器就足够了,简单高效。
当然,选择之后必须通过性能测试和监控来验证,并调整相应的 JVM 参数(如堆大小、新生代比例、GC 触发阈值等),直到满足业务的 SLA。
深度解析
主流垃圾回收器速览
为了帮助你更清晰地理解选型,这里简单梳理一下各回收器的特点和适用场景:
| 回收器 | 类型 | 特点 | 适用场景 | JDK 状态 |
|---|---|---|---|---|
| Serial | 串行 | 单线程回收,STW(Stop-The-World)时间长 | 单核 CPU、堆内存小、客户端应用 | JDK 8 及以后 |
| Parallel Scavenge / Parallel Old | 并行 | 多线程回收,追求高吞吐量,STW 时间相对较长 | 多核、后台批处理、数据分析 | JDK 8 默认组合 |
| CMS | 并发 | 老年代并发收集,低停顿,但会产生碎片、CPU 敏感 | 重视延迟的 Web 应用(已过时) | JDK 9 起标记废弃,JDK 14 正式移除 |
| G1 | 并发 + 并行 | 分区式堆,可预测停顿,平衡吞吐与延迟 | 大堆内存(>6GB)、低延迟要求的服务端 | JDK 9+ 默认 |
| ZGC | 并发 | 超低停顿(<10ms),与堆大小基本无关 | 超大堆、对延迟有极致要求(如金融、游戏) | JDK 11 实验,JDK 15+ 生产可用 |
| Shenandoah | 并发 | 与 ZGC 类似,更早实现并发压缩 | 同 ZGC,对停顿敏感的场景 | JDK 12 引入,部分厂商支持 |
选型时如何分析?
-
明确性能指标
- 吞吐量 = 业务代码执行时间 / (业务代码执行时间 + GC 时间)。如果要求吞吐量 > 99%,Parallel 可能更合适。
- 暂停时间:业务能否接受 200ms 的停顿?还是必须 <10ms?
- 内存大小:堆内存是 4GB 还是 100GB?不同回收器对大堆的适应能力不同。
-
监控当前系统
- 通过
-Xlog:gc*(JDK 9+)或-XX:+PrintGCDetails(JDK 8)打印 GC 日志,用 GCeasy、GCViewer 等工具分析:- Young GC 和 Full GC 的频率
- 每次停顿的时间
- 是否出现内存碎片、晋升失败等问题
- 如果现有的回收器无法满足指标,就需要考虑切换。
- 通过
-
参数调优验证
- G1 示例:设置
-XX:MaxGCPauseMillis=200表示期望最大停顿 200ms,但不要设置过小(如 10ms),否则会导致 GC 过于频繁,反而降低吞吐量。同时可以调整-XX:G1HeapRegionSize、-XX:InitiatingHeapOccupancyPercent等。 - ZGC 示例:只需开启
-XX:+UseZGC,并设置合适的堆大小。ZGC 的调优参数相对较少,重点在于保证并发线程数足够。
- G1 示例:设置
常见误区
-
误区一:CMS 还是低延迟的首选。
很多老项目仍在用 CMS,但它在 JDK 9 已标记废弃,且存在内存碎片、并发模式失败等风险。新项目不建议再用。 -
误区二:G1 可以完全替代 Parallel。
G1 的目标是 “可预测停顿”,并不一定在所有场景下吞吐量都比 Parallel 高。对于 CPU 密集型后台任务,Parallel 可能吞吐量更优。 -
误区三:直接使用默认设置,不做测试。
不同服务器硬件(CPU 核心数、内存大小)和业务负载下,默认参数的表现差异很大。务必通过压测和监控来验证。 -
误区四:最新回收器(ZGC)一定更好。
ZGC 虽然延迟极低,但对内存和 CPU 有额外开销(如染色指针、读屏障),而且需要较新版本的 JDK。如果项目还在 JDK 8,升级 JDK 本身就是一个工程决策。
总结
选择 JVM 垃圾回收器,本质上是一场性能与场景的匹配。先明确业务对吞吐量和延迟的容忍度,再结合堆内存大小、CPU 资源,从 Parallel、G1、ZGC 等主流方案中筛选出候选,最后通过监控和压测数据来验证决策。没有一成不变的最佳配置,只有持续调优的动态平衡。