JDK 8到JDK 17升级实战:五大垃圾回收器原理与G1/ZGC调优指南
2026/8/21 21:08:30 网站建设 项目流程

这次我们来看一个 Java 开发者绕不开的实战话题:从 JDK 8 升级到 JDK 17,特别是垃圾回收器(GC)的完整拆解。很多团队还在用 JDK 8,但无论是性能、安全还是新特性,JDK 17 作为长期支持版本(LTS)都已经是主流选择。升级的核心挑战之一,就是垃圾回收器的变迁——JDK 8 默认的 Parallel Scavenge + Parallel Old 组合,在 JDK 17 中已被 G1 取代,而经典的 CMS 回收器更是被彻底移除。

如果你关心升级后的应用性能、停顿时间(STW)变化,或者想知道如何为你的服务选择合适的 GC,这篇文章可以直接收藏。我们会先理清五大垃圾回收器(Serial, Parallel, CMS, G1, ZGC)的核心机制与适用场景,再给出从 JDK 8 迁移到 JDK 17 时,关于 GC 调优的实操建议和避坑指南。

1. 核心能力速览:JDK 8 到 JDK 17 的 GC 演变

在深入细节之前,我们先通过一个表格快速把握这次升级中垃圾回收器的关键变化,这决定了你后续的调优方向。

能力项JDK 8 典型情况JDK 17 典型情况说明与影响
默认 GCParallel Scavenge (Young) + Parallel Old (Old)G1 (Garbage-First)JDK 9 起 G1 成为默认。目标是在高吞吐与低延迟间取得更好平衡。
CMS 状态可用 (-XX:+UseConcMarkSweepGC)已移除JDK 14 中废弃,JDK 17 中完全移除。升级时必须切换 GC。
新 GC 选项G1 已存在,ZGC/Shenandoah 无ZGC、Shenandoah 成为生产可用选项ZGC(低延迟)、Shenandoah(低停顿)为超大堆或低延迟场景提供新选择。
调优复杂度相对简单,参数直观相对复杂,需理解新算法G1 的调优逻辑与 Parallel/CMS 不同,需要重新学习。
目标场景吞吐量优先平衡吞吐与延迟,或极致低延迟现代应用更关注响应时间,G1/ZGC 的设计更贴合此趋势。
升级强制动作必须移除-XX:+UseConcMarkSweepGC参数否则 JVM 无法启动。这是升级时最先要检查的点。

简单来说,从 JDK 8 升级到 JDK 17,在垃圾回收方面你不是在微调,而是在换一套引擎。默认的 GC 换了,旧的选项没了,新的强大工具出现了。理解这五大回收器的原理,是做出正确选择的前提。

2. 五大垃圾回收器核心原理拆解

要做出明智的选择,必须理解它们是如何工作的。我们按出现时间和设计目标,逐一拆解这五大回收器。

2.1 Serial 收集器:单线程的奠基者

这是最古老、最简单的收集器。它的“单线程”意义深远:在进行垃圾回收时,必须暂停所有用户线程(Stop-The-World, STW),并且这个暂停是由单个线程完成的。

  • 工作过程

    1. 新生代回收:采用“复制”算法。暂停所有线程,单个 GC 线程将 Eden 区和一个 Survivor 区中存活的对象复制到另一个空的 Survivor 区,然后清空 Eden 和之前的 Survivor。
    2. 老年代回收:采用“标记-整理”算法。同样暂停所有线程,单个 GC 线程标记出所有存活对象,然后将它们向内存空间的一端滑动,清理掉边界以外的内存。
  • 优点:实现简单,没有线程交互开销,在单核处理器或极小堆内存(如几十到百兆)的场景下效率很高。

  • 缺点:STW 时间与堆大小成正比,不适合现代多核服务器和大内存应用。

  • JDK 8/17 启用参数-XX:+UseSerialGC

  • 适用场景:客户端模式、嵌入式系统、或用于学习理解 GC 原理。生产环境服务器基本不用。

2.2 Parallel / Throughput 收集器:吞吐量的王者

这是 JDK 8 的默认组合,目标是最大化应用程序的吞吐量(单位时间内处理的业务量)。它是 Serial 收集器的多线程并行版本。

  • 工作过程

    1. 新生代:Parallel Scavenge,多线程并行执行复制算法。
    2. 老年代:Parallel Old,多线程并行执行标记-整理算法。
    3. 两者在进行回收时,都会发生 STW,但利用多核优势,缩短了 STW 的时间。
  • 优点:吞吐量高,适合后台运算、批处理等不太关心个别请求延迟的场景。

  • 缺点:STW 时间依然不可控,在堆较大时,一次 Full GC 的停顿可能达到数秒甚至更久。

  • JDK 8/17 启用参数-XX:+UseParallelGC(年轻代) 和-XX:+UseParallelOldGC(老年代,通常自动启用)

  • 适用场景:科学计算、数据导出、报表生成等吞吐量优先的业务。

2.3 CMS 收集器:低延迟的尝试(已退役)

Concurrent Mark Sweep 收集器的设计目标是获取最短的回收停顿时间。它允许垃圾收集线程与用户线程并发工作。

  • 工作过程(复杂,分阶段)

    1. 初始标记:STW,仅标记 GC Roots 直接关联的对象,速度很快。
    2. 并发标记:GC 线程与用户线程并发,遍历整个对象图进行标记。
    3. 重新标记:STW,修正并发标记期间因用户线程运行而产生变动的标记。
    4. 并发清除:GC 线程与用户线程并发,清理未被标记的死亡对象。
  • 优点:大部分标记和清除工作与应用并发,STW 时间极短,用户体验好。

  • 缺点

    • CPU 敏感:并发阶段会占用一部分 CPU,导致应用吞吐量下降。
    • 浮动垃圾:并发清理阶段产生的垃圾只能下次 GC 处理。
    • 内存碎片:采用“标记-清除”算法,会产生碎片,可能触发 Full GC 进行压缩。
    • 调优复杂:参数繁多,如-XX:CMSInitiatingOccupancyFraction设置触发阈值。
  • JDK 8 启用参数-XX:+UseConcMarkSweepGC

  • 现状JDK 14 废弃,JDK 17 移除。升级时必须替换。

  • 历史场景:曾经用于对延迟敏感的服务,如 Web 服务器。

2.4 G1 收集器:平衡之选(JDK 9+ 默认)

Garbage-First 的设计目标是替代 CMS,在可控的停顿时间内获得尽可能高的吞吐量。它引入了“Region”的概念,将堆划分为多个大小相等的独立区域。

  • 核心思想

    1. 化整为零:不再固定分代物理边界,每个 Region 可以属于 Eden、Survivor、Old 或 Humongous(存放大对象)。
    2. 可预测停顿:通过-XX:MaxGCPauseMillis参数设定目标停顿时间(如 200ms),G1 会尽力达成。
    3. 筛选回收:根据每个 Region 的“垃圾价值”(回收所需时间与可释放空间),优先回收价值高的 Region(Garbage-First 名字由来)。
  • 工作过程

    1. 年轻代回收:STW,多线程并行将 Eden 和 Survivor Region 的存活对象复制到新的 Survivor 或 Old Region。
    2. 并发标记周期:类似 CMS,但作用于整个堆,用于标记老年代 Region 的存活对象,并计算各 Region 的回收价值。
    3. 混合回收:在并发标记周期后,G1 会多次进行混合回收,不仅回收年轻代 Region,还会根据价值选择部分老年代 Region 进行回收。
  • 优点:兼顾吞吐和停顿,可预测停顿时间,有效避免内存碎片。

  • 缺点:内存占用稍高(Remembered Set 等数据结构开销),调优参数比 Parallel 多。

  • JDK 8/17 启用参数-XX:+UseG1GC(JDK 9 后默认)

  • 适用场景JDK 9+ 的通用默认选择,适用于大多数服务端应用,特别是堆内存较大(如 6GB 以上)或对停顿时间有要求的服务。

2.5 ZGC 收集器:极致低延迟的先锋

Z Garbage Collector 是 JDK 11 引入的实验特性,在 JDK 15 成为生产可用,目标是将停顿时间控制在10 毫秒以内,且停顿时间不会随堆大小增长而增加。

  • 核心技术

    1. 染色指针:将 GC 相关的元数据存储在指针本身,而非对象头,这减少了内存访问开销。
    2. 并发处理:标记、转移(压缩)、重定位等几乎所有阶段都是并发执行的,STW 时间极短,仅用于根节点扫描等必要环节。
    3. Region 布局:支持动态创建和销毁的 Region,更灵活。
  • 优点:超低停顿(亚毫秒到十毫秒级),停顿时间与堆大小无关,吞吐量损失小。

  • 缺点:在 JDK 17 中,不支持类卸载(JDK 21 已支持),内存占用较高。

  • JDK 17 启用参数-XX:+UseZGC

  • 适用场景:超大堆内存(TB 级别)、对延迟极度敏感的核心交易系统、实时计算。需要评估其内存开销和功能限制。

Shenandoah是另一个低停顿收集器,目标与 ZGC 类似,但实现原理不同(使用 Brooks 指针)。启用参数为-XX:+UseShenandoahGC。在 JDK 17 中,ZGC 更受 Oracle 官方推荐。

3. 从 JDK 8 (CMS/Parallel) 迁移到 JDK 17 (G1) 实战指南

理论清楚了,现在进入实战。假设你有一个正在使用 JDK 8 和 CMS 收集器的线上服务,如何平稳升级到 JDK 17?

3.1 环境准备与前置检查

  1. 备份与回滚方案:这是第一步。确保有完整的代码、配置备份,并规划好快速回滚到 JDK 8 的流程。
  2. 检查依赖兼容性
    • 框架/库版本:确保 Spring Boot、MyBatis、Netty 等核心框架支持 JDK 17。通常 Spring Boot 2.5+ 已提供良好支持。
    • 第三方 JAR 包:某些古老的或使用了内部 API 的 JAR 可能在 JDK 17 上运行失败。使用jdeps工具进行初步分析。
    # 分析应用 jar 对 JDK 内部 API 的依赖 jdeps --jdk-internals --class-path "lib/*" your-application.jar
    • 移除 Nashorn:JDK 15 移除了 Nashorn JavaScript 引擎。如果项目中使用,需迁移到 GraalVM JavaScript 等替代方案。
  3. 清理废弃的 JVM 参数
    • 首要任务:删除所有-XX:+UseConcMarkSweepGC-XX:+UseParNewGC参数。
    • 检查其他废弃参数:如-XX:+CMSClassUnloadingEnabled等 CMS 相关参数也应移除。启动时 JVM 会警告废弃参数,需关注日志。

3.2 安装部署与启动方式

  1. 下载 JDK 17:从 Adoptium (原 AdoptOpenJDK)或 Oracle 官网下载对应系统的 JDK 17 LTS 版本。
  2. 配置环境变量:更新JAVA_HOMEPATH指向 JDK 17 目录。
  3. 首次启动(无参数):先使用 JDK 17 默认设置(即 G1GC)启动应用,进行最基本的冒烟测试。
    # 假设原来启动命令是: # java -Xms2g -Xmx2g -XX:+UseConcMarkSweepGC -jar app.jar # 升级后,先简化为: java -Xms2g -Xmx2g -jar app.jar
  4. 验证启动与功能:确保应用能正常启动,核心业务流程跑通。

3.3 性能基准测试与 GC 日志分析

升级后,GC 行为变了,必须进行性能对比测试。

  1. 开启 GC 日志:这是最重要的调优依据。在启动参数中添加:

    -Xlog:gc*,gc+heap=debug,gc+age=trace:file=gc_%p_%t.log:time,uptime,level,tags:filecount=10,filesize=100M
    • 这个参数在 JDK 9+ 的 Unified Logging 框架下,会输出非常详细的 GC 日志。
    • filecountfilesize用于日志滚动,防止磁盘写满。
  2. 进行压力测试:使用 JMeter、wrk 或生产类似的流量,对升级前后的应用进行压测。记录:

    • 吞吐量(QPS/TPS)
    • 平均响应时间、P95/P99 响应时间
    • JVM 的 CPU 和内存使用情况
  3. 分析 GC 日志:关注以下关键指标:

    • Young GC 频率与耗时:G1 的年轻代回收是否过于频繁?平均耗时多少?
    • Mixed GC 情况:G1 是否启动了混合回收?回收了哪些老年代 Region?
    • Full GC出现 Full GC 是警报!G1 的设计目标之一就是避免 Full GC。如果出现,说明配置可能不合理或存在内存问题。
    • 停顿时间:对比 CMS 时代的停顿时间,G1 是否满足-XX:MaxGCPauseMillis的目标(默认 200ms)?

3.4 G1 调优参数建议(从 CMS 迁移而来)

如果默认 G1 表现不佳,可以尝试以下调优,但记住调优的原则是“先测量,后调优”

调优目标关键 JVM 参数说明与建议
控制停顿时间-XX:MaxGCPauseMillis=200G1 的目标停顿时间。设为应用可接受的范围(如 100-200ms)。设得太小会导致 GC 更频繁,反而降低吞吐。
设置堆大小-Xms4g -Xmx4g建议设为相同值,避免堆动态调整带来的额外 GC。G1 适合大堆,建议至少 4GB 以上。
调整并行线程数-XX:ParallelGCThreads=n并行阶段(STW阶段)的GC线程数。默认为CPU核数。如果GC线程占用CPU过多,可适当调小。
调整并发线程数-XX:ConcGCThreads=n并发阶段(标记阶段)的GC线程数。默认为ParallelGCThreads / 4。增加此值可加快并发标记,但会占用更多应用CPU。
调整 Region 大小-XX:G1HeapRegionSize=nRegion 大小,可为 1M 到 32M,必须是2的幂。堆内存很大时(如>32G),可考虑设置为 16M 或 32M。通常不需要改。
触发混合回收的阈值-XX:InitiatingHeapOccupancyPercent=45堆占用率达到多少时启动并发标记周期。默认45%。如果老年代增长快,可以适当降低此值,让 G1 更早开始标记。
处理大对象-XX:G1MixedGCLiveThresholdPercent=85-XX:G1HeapWastePercent=5控制混合回收中哪些老年代Region会被回收。如果大对象多,可以调整这些参数。

一个从 CMS 迁移过来的示例启动参数:

# 原 CMS 参数(JDK 8) # java -Xms4g -Xmx4g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -jar app.jar # 迁移后的 G1 参数(JDK 17) java -Xms4g -Xmx4g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:InitiatingHeapOccupancyPercent=40 \ -Xlog:gc*,gc+heap=debug:file=gc.log:time,uptime,level,tags:filecount=5,filesize=50M \ -jar app.jar

4. 何时考虑 ZGC 或 Shenandoah?

如果你的应用满足以下特征,可以考虑跳过 G1,直接评估 ZGC:

  1. 堆内存非常大:超过 32GB,甚至达到数百 GB。
  2. 对停顿时间极度敏感:要求 P99 响应时间稳定,且停顿不能超过 10-20ms,例如金融支付、实时竞价、游戏服务器。
  3. 愿意付出额外资源:ZGC 在并发阶段会消耗更多的 CPU 和内存带宽,并且内存占用(特别是堆外内存)会比 G1 高。

启用 ZGC 非常简单:

java -Xms32g -Xmx32g -XX:+UseZGC -jar your-app.jar

对于 ZGC,通常只需要设置堆大小和启用开关。其核心优势就是“自动”,试图减少调优参数。但务必在测试环境充分压测,观察其 CPU 和内存开销是否符合预期。

5. 常见问题与排查方法

升级过程中,你可能会遇到以下问题:

问题现象可能原因排查方式解决方案
应用启动失败,报Unrecognized VM option 'UseConcMarkSweepGC'JDK 17 中已移除 CMS 相关参数检查启动脚本和配置文件的 JVM 参数彻底删除所有-XX:+UseConcMarkSweepGC-XX:+UseParNewGC等参数
升级后,Full GC 频繁或停顿时间变长1. G1 自适应调整期
2. 堆大小或 Region 设置不合理
3. 对象分配速率过快
分析 GC 日志,观察 Young/Mixed GC 频率和耗时;使用jstat -gcutil监控1. 给予 JVM 预热时间(如30分钟)
2. 调整-XX:MaxGCPauseMillis-XX:InitiatingHeapOccupancyPercent
3. 检查代码是否存在内存泄漏或过度分配
启用 ZGC 后,出现OutOfMemoryError: Metadata space或 Native Memory 增长ZGC 在 JDK 17 中内存占用较高,且不支持类卸载使用 NMT 监控 Native Memory:-XX:NativeMemoryTracking=detail1. 增加 Metaspace 大小:-XX:MaxMetaspaceSize=512m
2. 如果类加载频繁,考虑升级到JDK 21(ZGC 支持类卸载)或暂用 G1
压测时吞吐量下降明显GC 线程占用过多 CPU,或目标停顿时间设置过小监控系统 CPU 使用率,区分应用线程和 GC 线程1. 适当调整-XX:ParallelGCThreads-XX:ConcGCThreads
2. 放宽-XX:MaxGCPauseMillis
年轻代 GC 异常频繁Eden 区过小,或对象过早晋升分析 GC 日志中 Young GC 间隔和晋升大小1. 无需直接设置年轻代大小,G1 会自动调整
2. 检查代码中是否存在大量短命大对象或不当的缓存

6. 最佳实践与使用建议

  1. 优先使用默认值:无论是 G1 还是 ZGC,现代 JVM 的默认配置都经过了广泛测试。不要一上来就调参。先使用默认配置进行压测,根据 GC 日志再决定是否需要调优。
  2. 理解监控指标:学会使用jstatjcmd、GC 日志以及 APM 工具(如 Prometheus + Grafana)监控 GC 行为。关键指标:GC 频率、各阶段耗时、STW 总时间、内存使用趋势。
  3. 堆大小设置黄金法则-Xms-Xmx设置为相同值。避免堆自动扩容收缩带来的性能抖动。
  4. 面向低延迟与高吞吐的抉择
    • 追求吞吐量:可以继续使用-XX:+UseParallelGC(Parallel Scavenge + Parallel Old),它在 JDK 17 中依然可用。
    • 追求低延迟:默认的 G1 是安全且平衡的选择。堆超大且追求极致延迟,再考虑 ZGC。
  5. 升级路径:对于复杂核心应用,建议分阶段升级:JDK 8 -> JDK 11 (LTS) -> JDK 17 (LTS)。JDK 11 是一个重要的中间版本,许多内部 API 和 GC 的变更已发生,在此版本验证兼容性更稳妥。
  6. 测试环境充分验证:升级必须在模拟生产环境的测试集群进行长时间(至少数天)的压测和稳定性测试,观察高峰、平峰期的 GC 表现。

从 JDK 8 升级到 JDK 17,垃圾回收器的切换是技术升级的核心环节。放弃熟悉的 CMS,拥抱 G1 或探索 ZGC,需要理念上的转变:从“手动精细调优”到“相信 JVM 的自动化与智能化”。掌握这五大回收器的工作原理,能让你在升级时心中有图,调优时手下有度。建议从默认的 G1 开始,打开详细的 GC 日志,让数据驱动你的决策,这才是应对升级挑战最可靠的方法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询