
最近在负责一个老项目的技术栈升级从 JDK 8 迁移到 JDK 17。过程中除了要处理语法兼容性、模块化等问题最核心也最容易踩坑的环节莫过于垃圾回收器的选择和调优。JDK 8 时代我们可能对 Parallel Scavenge Parallel Old (PSPO) 或 CMS 习以为常但到了 JDK 17G1 已成为默认ZGC 和 Shenandoah 也崭露头角选择多了困惑也多了。本文旨在为你提供一份从 JDK 8 升级到 JDK 17 时关于垃圾回收器的完整实战指南。我们将系统拆解 JDK 8 到 JDK 17 中五大核心垃圾回收器Serial, Parallel, CMS, G1, ZGC的工作原理、适用场景、关键参数及升级切换时的具体操作。无论你是正在规划升级的架构师还是需要解决升级后性能问题的开发者都能从中找到清晰的路径和可落地的方案。1. 背景与核心概念为什么升级与GC为何关键Java 应用的性能、稳定性和资源利用率与垃圾回收器Garbage Collector, GC的选择息息相关。从 JDK 8 升级到 JDK 17并非简单的版本数字变化而是一次包含语言特性、运行时性能和内存管理模型的全面演进。1.1 升级 JDK 17 的核心驱动力长期支持LTSJDK 17 是继 JDK 11 之后的又一个 LTS 版本提供长期的安全更新和支持对于生产环境至关重要。性能提升包括新的 GC 算法如 ZGC 的持续优化、即时编译器JIT的改进以及基础库的性能优化。新语言特性Records、Sealed Classes、Pattern Matching 等特性提升了开发效率和代码质量。模块化JPMS虽然 JDK 9 引入但在 JDK 17 中更为成熟有助于构建更安全、更轻量的应用。旧版本淘汰JDK 8 已发布多年其非 LTS 更新已停止继续使用存在安全风险。1.2 垃圾回收器内存的“清洁工”Java 程序在堆内存中创建对象。垃圾回收器的作用就是自动识别并回收那些不再被程序使用的对象即“垃圾”释放内存空间以供复用。GC 的过程通常包括两个阶段标记Mark遍历所有存活的对象并进行标记。清除Sweep回收未被标记的垃圾对象所占用的空间。不同的 GC 器在如何执行“标记-清除”以及何时执行上策略迥异从而在吞吐量Throughput、延迟Latency和内存占用Footprint这三个核心指标上做出不同的权衡。1.3 从 JDK 8 到 JDK 17 的 GC 演变在 JDK 8 中常用的组合是-XX:UseParallelGC(Parallel Scavenge Parallel Old) 或-XX:UseConcMarkSweepGC(CMS)。JDK 9 是一个重要转折点G1 被设置为默认垃圾回收器。到了 JDK 17G1 依然是默认选择但面向低延迟的 ZGC 和 Shenandoah 已经非常成熟并移除了 CMS 和 Concurrent Mode Failure 风险较高的组合。对于升级者来说理解这种演变并为自己应用选择最合适的 GC是保证升级平稳、性能不降反升的关键。2. 环境准备与版本说明在深入 GC 细节之前我们先明确实验和讲解的环境。请注意生产环境的升级务必先在测试环境充分验证。2.1 基础环境操作系统Linux (CentOS 7.9) / Windows 10 / macOSJDK 版本对比基线JDK 8u381 (或类似更新版本)升级目标JDK 17.0.10 (推荐使用 Oracle OpenJDK 或 Eclipse Temurin 发行版)构建工具Maven 3.6 或 Gradle 7IDEIntelliJ IDEA 2023.1 或 Eclipse需配置多 JDK 环境。2.2 如何安装与切换多版本 JDK以 Linux/Mac 为例使用tar.gz包安装并管理# 1. 下载 JDK (以 Eclipse Temurin 为例) wget https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.10%2B7/OpenJDK17U-jdk_x64_linux_hotspot_17.0.10_7.tar.gz wget https://github.com/adoptium/temurin8-binaries/releases/download/jdk8u412-b08/OpenJDK8U-jdk_x64_linux_hotspot_8u412b08.tar.gz # 2. 解压到指定目录如 /usr/local/java/ tar -zxvf OpenJDK17U-jdk_x64_linux_hotspot_17.0.10_7.tar.gz -C /usr/local/java/ tar -zxvf OpenJDK8U-jdk_x64_linux_hotspot_8u412b08.tar.gz -C /usr/local/java/ # 3. 配置环境变量 (在 ~/.bashrc 或 ~/.zshrc 中) export JAVA_8_HOME/usr/local/java/jdk8u412-b08 export JAVA_17_HOME/usr/local/java/jdk-17.0.107 export JAVA_HOME$JAVA_17_HOME # 默认使用 JDK 17 export PATH$JAVA_HOME/bin:$PATH # 4. 使配置生效并验证版本 source ~/.bashrc java -version在 IDEA 中可以进入File - Project Structure - SDKs添加多个 JDK然后在Project设置中选择项目使用的 SDK 和语言级别。2.3 关键诊断工具准备GC 调优离不开监控和日志命令行工具jps,jstat,jmap,jstack(包含在 JDK 的bin目录下)。图形化工具JConsole, VisualVM, JDK Mission Control (JMC)。GC 日志必须开启。这是分析 GC 行为最重要的依据。3. 五大垃圾回收器核心原理拆解本章将深入剖析 Serial, Parallel, CMS, G1, ZGC 这五大回收器。我们会从设计目标、堆内存划分、工作流程和优缺点四个方面进行对比讲解。3.1 Serial / Serial Old 收集器设计目标单线程、简单高效适用于客户端模式或微小型应用。堆结构采用经典的新生代Young Gen和老年代Old Gen分代设计。新生代使用“复制”算法老年代使用“标记-整理”算法。工作流程进行垃圾回收时必须暂停所有应用线程Stop-The-World, STW。单线程完成垃圾标记和清理工作。启动参数-XX:UseSerialGC优点实现简单没有线程交互开销在单核处理器或极小堆内存如几十MB下可能效率最高。缺点STW 时间随堆内存增大而显著增长完全不适合服务端应用。JDK 8~17 状态一直存在作为兜底和特定场景使用。3.2 Parallel Scavenge / Parallel Old (PSPO) 收集器设计目标高吞吐量Throughput。旨在最大化应用程序的执行时间适合后台计算、批处理任务。堆结构同样是分代模型。其新生代收集器称为 Parallel Scavenge老年代收集器称为 Parallel Old。工作流程多线程并行进行垃圾回收但依然会发生 STW。专注于减少总体的 GC 时间而非每次 GC 的停顿时间。启动参数-XX:UseParallelGC(新生代并行) 和-XX:UseParallelOldGC(老年代并行通常与前者联用)。关键调优参数-XX:ParallelGCThreads设置并行 GC 线程数默认为 CPU 核心数。-XX:MaxGCPauseMillis设置期望的最大 GC 停顿时间毫秒收集器会尽力但不保证达到。-XX:GCTimeRatio设置吞吐量目标GC时间与总时间的比率默认为 99即 GC 时间不超过 1%。优点在多核环境下能有效利用系统资源获得更高的吞吐量。缺点STW 停顿时间相对不可控尤其在堆较大或对象存活率较高时。JDK 8~17 状态在 JDK 8 中是默认 GC 之一在 JDK 17 中依然重要是吞吐量优先场景的首选。3.3 Concurrent Mark-Sweep (CMS) 收集器设计目标低延迟Low Latency。通过并发标记来减少 STW 时间适合对响应时间敏感的应用。堆结构分代模型。其老年代收集是并发的。工作流程四阶段初始标记Initial MarkSTW标记 GC Roots 直接关联的对象速度很快。并发标记Concurrent Mark与应用线程并发遍历整个老年代对象图。重新标记RemarkSTW修正并发标记期间因应用线程运行而产生变动的标记。并发清除Concurrent Sweep与应用线程并发清理垃圾对象。启动参数-XX:UseConcMarkSweepGC关键调优参数-XX:CMSInitiatingOccupancyFraction老年代空间使用率触发 CMS 的阈值如 75%。设置过高易引发 Concurrent Mode Failure。-XX:UseCMSInitiatingOccupancyOnly强制使用上面设定的阈值而不是由 JVM 动态调整。-XX:CMSParallelRemarkEnabled启用并行重新标记减少 Remark 阶段的 STW 时间。优点大部分标记和清除工作与应用线程并发显著降低了老年代收集的停顿时间。缺点内存碎片使用“标记-清除”算法会产生内存碎片可能导致 Full GC。并发模式失败Concurrent Mode Failure如果在并发清理完成前老年代空间被填满JVM 会退化为 Serial Old 收集器进行 Full GC导致长时间 STW。对 CPU 资源敏感并发阶段会与应用线程竞争 CPU。JDK 8~17 状态在 JDK 14 中被标记为废弃Deprecated在 JDK 17 中已被移除。这是从 JDK 8 升级时必须注意的重大变化。3.4 Garbage-First (G1) 收集器设计目标在可控的停顿时间如几百毫秒内获得高吞吐量。取代 CMS成为 JDK 9 及以后的默认收集器。堆结构取消了物理上的新生代、老年代连续空间划分将整个堆划分为多个大小相等的独立区域Region。每个 Region 可能扮演 Eden、Survivor 或 Old 角色。工作流程核心是“回收价值最大化”年轻代收集Young GCSTW对 Eden 和 Survivor Region 进行回收。并发标记周期Concurrent Marking Cycle类似 CMS但作用于整个堆。识别出各个 Region 的“垃圾比例”回收价值。混合收集Mixed GCSTW不仅收集年轻代 Region还会根据用户设定的停顿时间目标选择一部分垃圾比例高的老年代 Region 进行回收。这是 G1 实现可控停顿的关键。启动参数-XX:UseG1GC(JDK 9 默认)关键调优参数-XX:MaxGCPauseMillis期望的最大停顿时间目标默认 200ms。G1 会尽力达到此目标。-XX:G1HeapRegionSize设置 Region 大小1MB~32MB2的幂。一般无需手动设置。-XX:InitiatingHeapOccupancyPercent触发并发标记周期的堆占用率阈值默认 45%。优点可控的停顿时间通过 Mixed GC 分批次回收避免了一次性回收大量老年代导致的长时间 STW。整体吞吐量良好兼顾了延迟和吞吐量。有效处理大内存Region 设计更适合大堆如 4G 以上。缺点比 Parallel 和 CMS 更复杂在小堆或极高吞吐量要求的场景下可能不如 Parallel。JDK 8~17 状态JDK 8 中需手动启用JDK 9 起成为默认是当前最主流、最平衡的服务端 GC。3.5 Z Garbage Collector (ZGC)设计目标亚毫秒级Sub-millisecond的超低停顿时间且停顿时间不随堆大小或存活对象集大小而增长。适用于超大内存TB 级和极致低延迟场景。堆结构同样采用 Region 划分称为 ZPage但支持动态创建和销毁。核心技术并发处理标记、转移压缩、重定位指针等所有耗时操作几乎都是并发的。染色指针Colored Pointers在 64 位指针中嵌入元数据如标记位、重映射信息使得 GC 状态信息与对象本身解耦无需遍历对象即可获取信息极大提升了并发效率。负载屏障Load Barrier在应用线程从堆中加载对象引用时执行一小段代码来协助完成并发转移等操作。启动参数-XX:UseZGC关键调优参数-Xmx设置最大堆内存。ZGC 处理大堆能力极强。-XX:ConcGCThreads并发 GC 线程数。-XX:SoftMaxHeapSizeJVM 会努力将堆大小维持在此值以下。优点超低停顿通常小于 10ms甚至 1ms 以内。可扩展性停顿时间与堆大小无关。高吞吐量在低延迟目标下仍能保持不错的吞吐量。缺点更高的 CPU 和内存开销并发操作和负载屏障会带来额外开销。JDK 版本要求在 JDK 15 中才成为生产可用特性。JDK 8~17 状态JDK 11 作为实验特性引入JDK 15 成为生产特性。是 JDK 17 中面向未来的高端选择。4. 从 JDK 8 升级到 JDK 17GC 切换实战与参数迁移了解了原理我们来看实战。升级的核心步骤是备份、测试、切换、监控、调优。4.1 升级前准备备份与基准测试完整备份备份当前 JDK 8 环境下的应用代码、配置、数据和启动脚本。建立基准在 JDK 8 环境下使用生产类似的负载收集关键性能指标和 GC 日志作为对比基线。# JDK 8 基准测试启动参数示例 (使用 Parallel GC) java -Xms4g -Xmx4g \ -XX:UseParallelGC -XX:UseParallelOldGC \ -XX:PrintGCDetails -XX:PrintGCDateStamps -XX:PrintGCTimeStamps \ -Xloggc:/path/to/gc.log \ -jar your-application.jar4.2 升级步骤与 GC 选择策略安装 JDK 17如第 2 章所示在测试环境安装 JDK 17。编译与语法兼容性确保项目代码在 JDK 17 下编译通过。注意移除或替换已废弃的 API如sun.misc.*下的类。选择新的 GC这是最关键的一步。根据你的应用类型选择通用 Web 服务/微服务首选 G1(-XX:UseG1GC)。它是默认值平衡性好适合大多数场景。直接从 JDK 8 的 Parallel 或 CMS 切换到 G1。批处理/计算密集型应用如果对吞吐量极其敏感且能容忍较长停顿可继续使用Parallel GC(-XX:UseParallelGC)。它在 JDK 17 中依然表现强劲。高并发、低延迟、大内存应用如交易系统、实时推荐、大数据平台。强烈考虑 ZGC(-XX:UseZGC)。需要评估 CPU 资源是否充足。CMS 用户必须更换因为 CMS 已移除。根据上述原则迁移到 G1 或 ZGC。4.3 GC 参数迁移与配置示例假设你有一个在 JDK 8 上使用 CMS 的 Spring Boot 应用堆大小为 8G现在要迁移到 JDK 17。JDK 8 (CMS) 原有参数:java -Xms8g -Xmx8g \ -XX:UseConcMarkSweepGC \ -XX:CMSInitiatingOccupancyFraction75 \ -XX:UseCMSInitiatingOccupancyOnly \ -XX:ExplicitGCInvokesConcurrent \ -XX:PrintGCDetails -Xloggc:/app/gc.log \ -jar app.jar迁移到 JDK 17 (G1) 推荐参数:java -Xms8g -Xmx8g \ -XX:UseG1GC \ # 启用 G1 -XX:MaxGCPauseMillis200 \ # 设置停顿时间目标 -XX:G1HeapRegionSize4m \ # 根据堆大小可选8G堆可用4m -XX:InitiatingHeapOccupancyPercent45 \ # 并发标记触发阈值 -Xlog:gc*,gcheapdebug,gcergo*trace,gcage*debug:file/app/gc.log:time,uptime,level,tags:filecount10,filesize10m \ -jar app.jar参数变化解读-XX:UseConcMarkSweepGC--XX:UseG1GC核心切换。-XX:CMSInitiatingOccupancyFractionG1 中用-XX:InitiatingHeapOccupancyPercent替代含义类似。-XX:ExplicitGCInvokesConcurrent对于 G1System.gc()默认是并发的 Full GC此参数通常无需显式设置。GC 日志格式巨变JDK 9 引入了统一日志框架-Xlog。新的日志格式更强大但需要重新学习。上面的示例是一个功能丰富的 G1 日志配置。迁移到 JDK 17 (ZGC) 推荐参数:java -Xms8g -Xmx8g \ -XX:UseZGC \ -XX:ZGenerational \ # JDK 21引入分代ZGC性能更好。JDK 17可用非分代。 # -XX:ConcGCThreads2 \ # 可调整并发线程数 -Xlog:gc*,gcheapdebug,gcstatsoff:file/app/gc.log:time,uptime,level,tags:filecount10,filesize10m \ -jar app.jar注意分代 ZGC (-XX:ZGenerational) 在 JDK 21 引入性能提升显著。如果追求极致性能且可升级到 JDK 21建议使用。JDK 17 中使用非分代 ZGC。4.4 验证与监控启动应用使用新的 JDK 17 和 GC 参数启动应用。检查日志确保没有因版本不兼容导致的启动错误。监控 GC 行为# 使用 jstat 实时查看 GC 情况 (每1秒采样一次) jstat -gcutil pid 1000 # 使用 jcmd 获取 GC 信息 jcmd pid GC.heap_info分析 GC 日志将新的 GC 日志与 JDK 8 的基线日志进行对比。关注吞吐量[Eden, Old]区的回收频率和耗时。延迟Young GC 和 Full GC 的停顿时间Pause Time。内存使用老年代占用是否平稳是否有内存泄漏迹象。5. 常见问题与排查思路升级过程中你可能会遇到以下典型问题。问题现象可能原因排查思路与解决方案应用启动失败报UnsupportedClassVersionError编译环境的 JDK 版本高于运行环境的 JDK 版本。1. 检查java -version确认运行环境是 JDK 17。2. 使用 Maven/Gradle 指定正确的target编译版本如maven-compiler-plugin中设置release17/release。升级后CPU 使用率异常升高1. 新的 GC如 ZGC并发阶段占用 CPU。2. 存在循环调用System.gc()的代码。3. 应用自身存在性能问题。1. 使用top -Hp pid查看线程结合jstack分析高 CPU 线程。2. 检查 GC 日志看 GC 活动是否过于频繁。对于 ZGC可适当调整-XX:ConcGCThreads。3. 使用-XX:DisableExplicitGC禁用显式 GC 调用需谨慎确保第三方库不依赖它。升级后内存使用量增加1. JDK 17 的元空间Metaspace或堆外内存管理有变化。2. G1/ZGC 本身需要额外的内存开销如 Remembered Set。3. 应用存在内存泄漏。1. 使用jcmd pid VM.native_memory详细分析内存组成。2. 对比 JDK 8 和 JDK 17 下jmap -heap的输出。3. 使用-XX:MaxMetaspaceSize限制元空间大小避免无限增长。从 CMS 切换到 G1 后出现长时间停顿1. G1 的 Mixed GC 回收速度跟不上对象分配速度触发 Full GC。2.-XX:MaxGCPauseMillis设置过小导致 G1 回收效率低下。3. 大对象Humongous Object分配过多。1. 分析 GC 日志确认停顿发生在 Young GC、Mixed GC 还是 Full GC。2.适当调大-XX:MaxGCPauseMillis如从 200 调到 300给 G1 更多时间做更高效的回收。3. 增加堆大小-Xmx。4. 使用-XX:G1HeapRegionSize调整 Region 大小使大对象能被正常管理。使用 ZGC 时出现Allocation Stall或性能下降1. 堆内存不足ZGC 的并发回收速度跟不上分配速度。2. CPU 资源严重不足无法支撑 ZGC 的高并发开销。1.增加堆内存-Xmx。ZGC 是为大内存设计的不要吝啬。2. 确保有足够的 CPU 核心。ZGC 的并发线程会占用 CPU。3. 考虑升级到 JDK 21 使用分代 ZGC其分配性能和吞吐量有大幅提升。GC 日志文件不生成或格式不对JDK 9 的 GC 日志参数格式已变更。使用新的-Xlog:gc*:file...格式如第 4.3 节示例所示。-XX:PrintGCDetails等旧参数在 JDK 9 后可能被忽略或产生不同输出。6. 最佳实践与工程建议6.1 升级路径规划循序渐进先在开发、测试环境验证再灰度到生产。不要直接全量切换。性能对比升级后必须进行全面的性能压测与 JDK 8 基线对比吞吐量、延迟P99, P999、资源使用率。监控先行确保监控系统如 Prometheus Grafana已集成 JVM 监控Micrometer, JMX Exporter能清晰看到 GC 时间、频率、内存池变化。6.2 GC 选择决策树面对多个 GC可以按以下流程决策是否追求极致吞吐量且可接受秒级停顿 是 - 选择 Parallel GC。 否 - 堆内存是否小于 4G且应用非常简单 是 - Serial GC 或 G1 均可。 否 - 是否要求亚毫秒级停顿且拥有充足 CPU 和内存资源 是 - 选择 ZGC (JDK 15) 或 Shenandoah。 否 - 选择 G1 GC (默认且推荐)。6.3 关键参数调优心得不要过度调优JVM 的 Ergonomics自适应优化已经非常智能。先使用默认参数运行根据 GC 日志反映出的问题再进行针对性调整。核心参数优先级-Xms和-Xmx必须设置成相等值避免堆震荡这对 G1 和 ZGC 的性能稳定至关重要。-XX:MaxGCPauseMillis(G1)这是一个目标值不是保证值。设置一个合理的、业务能接受的停顿时间如 200ms。堆大小在物理内存允许的情况下给足堆空间。内存是最廉价的资源之一能有效减少 GC 频率。关注 Full GC在现代 GCG1, ZGC中应极力避免 Full GC。一旦发生说明配置可能不合理或应用有内存问题。通过分析 GC 日志找到触发 Full GC 的原因。6.4 生产环境检查清单在将 JDK 17 和新 GC 配置推上生产前请核对[ ] 已在测试环境进行至少一周的稳定性运行。[ ] 核心业务接口的 P99 延迟未劣化。[ ] GC 日志中无频繁的Full GC或Allocation Failure。[ ] 监控告警已配置如 GC 时间超过阈值、老年代使用率持续过高。[ ] 回滚方案已准备就绪包括 JDK 8 的安装包和旧版启动参数。[ ] 团队核心成员熟悉新 GC 的基本原理和日志分析方法。从 JDK 8 升级到 JDK 17并妥善配置垃圾回收器是一次显著提升应用现代化水平和运行效能的机会。G1 作为默认且平衡的选择能满足绝大多数场景而 ZGC 则为未来面向低延迟、大内存的应用打开了新的大门。升级过程虽有挑战但通过系统的准备、科学的测试和谨慎的调优完全可以实现平稳过渡。建议将本文作为参考手册在升级的不同阶段反复查阅结合自己应用的具体表现找到最适合的那一组“神秘参数”。