这是一个面向工程师 / 运维的简化但详细的 Java HotSpot 垃圾回收(GC)调优手册。假设你的应用运行在 JDK 11/17+(以 Java 17 为例),目标是快速建立基线、选择合适的收集器、逐步调整并验证效果。
核心原则:先测量(baseline)→ 小步改动(single variable changes)→ 观测与回滚 → 迭代优化。
适用对象与场景
你负责线上或预发布的 Java 服务(Spring Boot、微服务、RPC 服务等)。
目标可能是:降低最大延迟(pause)、减少 Full GC、提高吞吐量或降低内存占用。
需要具备对线上采集日志、重启服务和调参权限的工程师。 (Oracle 文档)
必备工具(准备工作)
在开始前请确保能在目标机器上运行或采集下列工具/命令输出:
JVM 版本信息:
java -version
(确认是 HotSpot、版本) 。 (Oracle 文档)监控/诊断工具:
jcmd
、jstat
、jmap
、jstack
、jcmd
(用于触发 JFR / dump / gc 等),以及 Java Flight Recorder (JFR) / JDK Mission Control(用于详细性能采样)。 (Oracle 文档)GC 日志(必须):使用
-Xlog:gc*
或等价老写法开启到文件。示例见下。(jvmperf: JVM Performance Workshop)一个能够模拟真实负载的压测工具(wrk、ab、jmeter、gatling 等),用于可重复的性能测试。
步骤 0 — 记录基线(Baseline)
在不改任何 GC 参数的情况下运行一次代表性负载(生产或预发布流量)。记录:
平均/百分位延迟(p50 / p95 / p99)
吞吐量(requests/sec)
JVM heap 使用峰值、GC 停顿次数与最大停顿时间(pause)
CPU、IO、线程数指标
使用
jstat -gc <pid> 1000 10
观察短期内 Eden/Survivor/Old 使用情况;用jcmd <pid> VM.native_memory summary
(如需要)或jmap -heap <pid>
获取堆详情。(Oracle 文档)保存 GC 日志:如果程序未开启日志,先在测试环境用以下方式运行并采集日志(示例):
java -Xms4g -Xmx4g -XX:+UseG1GC \
-Xlog:gc*:file=/var/log/myapp_gc.log:time,uptime,level,tags \
-jar myapp.jar
(上面以 G1 为例;后文会按场景讲如何切换) 。(jvmperf: JVM Performance Workshop)
步骤 1 — 收集并开启观测(GC 日志 + JFR)
开启 GC 日志(必做)
现代 JDK 建议使用
-Xlog:gc*
:例如-Xlog:gc*:file=/path/gc.log:time,uptime,level
。这样可以记录每次 Minor/Full GC、停顿时长、回收前后堆占用等信息。(jvmperf: JVM Performance Workshop)
启用 Java Flight Recorder(建议在预发/长时间采样)
快速启动命令(不需重启 JVM 的话可用 jcmd):
jcmd <pid> JFR.start name=perf filename=/tmp/myapp.jfr duration=15m jcmd <pid> JFR.dump name=perf filename=/tmp/myapp.jfr
JFR 可帮助你定位 GC 以外的瓶颈(锁争用、方法热点、分配热点)。(Oracle 文档)
持续监控:将 GC 日志和 JFR 与 Prometheus/Grafana 或外部 APM(如 NewRelic、Dynatrace)结合,便于长期趋势分析。(Medium)
步骤 2 — 选择适合的 GC(快速决策)
按业务目标选择收集器(常见建议):
追求极致吞吐量、不太敏感停顿:Parallel GC(Throughput)。
需要延迟-吞吐量平衡、并希望控制最大停顿:G1(Java 9+ 的默认收集器)。(Oracle)
非常低停顿(tiny pauses),堆大或延迟敏感:ZGC 或 Shenandoah(如果你的 JVM 发行版支持)。注意:ZGC 更依赖于合理的
-Xmx
设置。(Oracle 文档)
如何切换(示例):
启用 G1(多数 JDK 9+ 默认):
-XX:+UseG1GC
启用 ZGC(若可用):
-XX:+UseZGC
(每次切换应在预发或受控环境验证) 。(Oracle 文档)
步骤 3 — 基本调优项(按收集器给出可直接复制的参数)
先只改一个参数,跑压测并记录差异;如果效果不好再回滚。
A. G1(常用且适配广)
主要目标:控制最大停顿、提前触发混合回收(避免 Full GC)。
推荐起点(示例):
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=45 \
-XX:ParallelGCThreads=8 \
-XX:ConcGCThreads=4 \
-Xms8g -Xmx8g
解释:
MaxGCPauseMillis=200
:建议目标最大停顿 200ms(G1 会尽力,但不能保证绝对值)。(Oracle 文档)InitiatingHeapOccupancyPercent=45
:老年代占用 45% 时就开始並發標記(更早触发可避免老年代突然涨满)。(Oracle 文档)ParallelGCThreads
/ConcGCThreads
:按机器核数微调(一般不超过核数),避免 GC 抢占过多 CPU。
其他可选(依情况):
-XX:G1NewSizePercent
/-XX:G1MaxNewSizePercent
:限制 young 大小。(Oracle 文档)
B. ZGC(低停顿优先)
要点:
ZGC 是并发收集器,对
-Xmx
非常敏感:你必须确保-Xmx
能覆盖 live-set 并留足 headroom。(Oracle 文档)
推荐起点(示例):
-XX:+UseZGC -Xms16g -Xmx16g -XX:ConcGCThreads=4
解释与注意:
给 ZGC 较大堆通常能提升稳定性,但也要观察分配速率与 live-set。可考虑开启 large pages 提升性能(
-XX:+UseLargePages
)。(GC easy - Universal Java GC Log Analyser)
C. Parallel / Throughput GC(需要最大吞吐)
-XX:+UseParallelGC -XX:ParallelGCThreads=16 -Xms8g -Xmx8g
适合 CPU 密集且不敏感短暂停顿的场景。
步骤 4 — 如何读 GC 日志(简化要点)
看 停顿时间(pause):关注 p95/p99 的 pause length;若某次 pause 非常大,找对应时间段的日志行(通常会标明原因:GC type、并发标记、混合回收、Full)。(jvmperf: JVM Performance Workshop)
看 频率:频繁的小 pause 可能意味着年轻代太小或分配速率高;长时间未发生老年代回收可能会造成一次性大型标记。
看 Promotion(晋升)/Allocation failure:大量晋升可能说明 Survivor 区太小或对象寿命模型不合。
如果看到 Full GC 频繁,则必须优先定位导致 Full GC 的根因(如元空间、直接内存耗尽、老年代碎片化)。(jvmperf: JVM Performance Workshop)
工具:可以用
gcviewer
、gceasy.io
或自建脚本解析 GC 日志,快速得到 pause distribution、young/full counts 等图表。(GC easy - Universal Java GC Log Analyser)
步骤 5 — 使用 JFR / jcmd / jstat 定位问题(实操)
用 jstat 快速看内存走向:
jstat -gc <pid> 1000 10
观察 Eden/S0/S1/Old/Perm(或 Metaspace)变化。(Oracle 文档)
用 jcmd 触发 JFR(参考上文)并在 JDK Mission Control 中打开
.jfr
文件查看热点、锁、分配热点。(Oracle 文档)堆快照 / histogram(短期内查看对象分布):
jcmd <pid> GC.class_histogram > /tmp/hist.txt
或用
jmap -histo:live <pid>
。这些输出能帮助你判断哪些类分配最频繁(Potential allocation hotspots)。(Oracle 文档)
步骤 6 — 验证与回滚(测试策略)
每次改动只改一项(比如只改
MaxGCPauseMillis
或只改InitiatingHeapOccupancyPercent
),并运行同一套压测脚本。比较基线与当前改动的关键指标(延迟分位、吞吐量、GC pause histogram、CPU 使用)。
若改动导致负面影响,立刻回滚到上一个稳定配置并记录原因。
将成功的配置在更长时间(数小时或天)运行以观察长期影响(内存泄漏/慢慢上升的内存等)。
常见问题与排查建议(快速清单)
频繁 Minor GC + 高暂停:增大 Young(通过 G1NewSizePercent / MaxGCPauseMillis 调整)或检查分配热点(JFR)。(Oracle 文档)
频繁 Full GC:检查 Metaspace、直接内存或老年代溢出;查看是否有大量对象晋升或长寿命对象。(jvmperf: JVM Performance Workshop)
改了参数没效果:确认 JVM 实际启动参数(进程的启动脚本/容器命令行),并确保没有容器/平台限制(k8s memory limit)导致看起来“参数无效”。
GC 占用过多 CPU:可能是 GC 线程数过多或 GC 触发过频,考虑减少
ParallelGCThreads/ConcGCThreads
或调整触发阈值。
典型 G1 调优示例(从 150ms 平均延迟优化到 100ms)
示例仅作参考(已在上文给出类似示例),总结流程:
基线:
-Xms16g -Xmx16g -XX:+UseG1GC
(记录 gc.log 与 JFR)。(Oracle 文档)根据 GC 日志发现:老年代回收启动太晚 → 加早触发阈值:
-XX:InitiatingHeapOccupancyPercent=45
。设定目标 pause:
-XX:MaxGCPauseMillis=200
。调整并行/并发线程数(
ParallelGCThreads
/ConcGCThreads
)以匹配 CPU 资源。结果:p95、p99 延迟下降,最大 pause 下降,吞吐量略微上升或持平(视负载而定)。(Oracle 文档)
最后检查表(部署到生产前)
已在预发环境跑至少 3 批稳定压测(每批 >= 30 分钟)并记录日志。
收集到的 GC 日志已被解析并保存(方便回溯)。(jvmperf: JVM Performance Workshop)
已启用 JFR 并检查热点(如必要)。(Oracle 文档)
有明确的回滚步骤(旧启动脚本/容器镜像)与告警阈值(内存、延迟、错误率)。
在生产逐步灰度发布(先 5% 流量 → 20% → 全量),每步都观察关键指标。
推荐阅读与参考
Oracle — HotSpot Virtual Machine Garbage Collection Tuning Guide(Java 17 官方指南)。(Oracle 文档)
Oracle — G1 Garbage Collector Tuning(G1 细节说明)。(Oracle 文档)
Oracle — ZGC Tuning Guide(ZGC 关键点:
-Xmx
、headroom 等)。(Oracle 文档)GC 日志 & 分析工具简介(如何记录并解析 GC 日志)。(jvmperf: JVM Performance Workshop)
诊断工具(jcmd/jstat/jmap/JFR 使用简介)。(Oracle 文档)
默认评论
Halo系统提供的评论