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

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

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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/

面试考察点

  1. 考察对主流垃圾回收器的理解:面试官想确认你是否熟悉 JVM 中常见的垃圾回收器(Serial、Parallel、CMS、G1、ZGC 等),以及它们各自的核心特点和工作机制。

  2. 考察根据业务场景做技术选型的能力:除了知道概念,更重要的是能否根据项目实际(如吞吐量要求、延迟敏感度、堆内存大小、硬件资源)做出合理的选择,并说出背后的权衡。

  3. 考察实战经验和调优意识:你是否在项目中真正使用过这些回收器?有没有通过监控 GC 日志、调整参数来优化过?选型后如何验证效果?

  4. 考察对 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 引入,部分厂商支持

选型时如何分析?

  1. 明确性能指标

    • 吞吐量 = 业务代码执行时间 / (业务代码执行时间 + GC 时间)。如果要求吞吐量 > 99%,Parallel 可能更合适。
    • 暂停时间:业务能否接受 200ms 的停顿?还是必须 <10ms?
    • 内存大小:堆内存是 4GB 还是 100GB?不同回收器对大堆的适应能力不同。
  2. 监控当前系统

    • 通过 -Xlog:gc*(JDK 9+)或 -XX:+PrintGCDetails(JDK 8)打印 GC 日志,用 GCeasyGCViewer 等工具分析:
      • Young GC 和 Full GC 的频率
      • 每次停顿的时间
      • 是否出现内存碎片、晋升失败等问题
    • 如果现有的回收器无法满足指标,就需要考虑切换。
  3. 参数调优验证

    • G1 示例:设置 -XX:MaxGCPauseMillis=200 表示期望最大停顿 200ms,但不要设置过小(如 10ms),否则会导致 GC 过于频繁,反而降低吞吐量。同时可以调整 -XX:G1HeapRegionSize-XX:InitiatingHeapOccupancyPercent 等。
    • ZGC 示例:只需开启 -XX:+UseZGC,并设置合适的堆大小。ZGC 的调优参数相对较少,重点在于保证并发线程数足够。

常见误区

  • 误区一:CMS 还是低延迟的首选。
    很多老项目仍在用 CMS,但它在 JDK 9 已标记废弃,且存在内存碎片、并发模式失败等风险。新项目不建议再用。

  • 误区二:G1 可以完全替代 Parallel。
    G1 的目标是 “可预测停顿”,并不一定在所有场景下吞吐量都比 Parallel 高。对于 CPU 密集型后台任务,Parallel 可能吞吐量更优。

  • 误区三:直接使用默认设置,不做测试。
    不同服务器硬件(CPU 核心数、内存大小)和业务负载下,默认参数的表现差异很大。务必通过压测和监控来验证。

  • 误区四:最新回收器(ZGC)一定更好。
    ZGC 虽然延迟极低,但对内存和 CPU 有额外开销(如染色指针、读屏障),而且需要较新版本的 JDK。如果项目还在 JDK 8,升级 JDK 本身就是一个工程决策。

总结

选择 JVM 垃圾回收器,本质上是一场性能与场景的匹配。先明确业务对吞吐量和延迟的容忍度,再结合堆内存大小、CPU 资源,从 Parallel、G1、ZGC 等主流方案中筛选出候选,最后通过监控和压测数据来验证决策。没有一成不变的最佳配置,只有持续调优的动态平衡。