JVM GC调优关键在于可观测性与场景适配,需先确认是否真为GC问题,再结合分配/晋升速率、Young GC频率与停顿、老年代趋势四大指标,选用jstat、GC日志、jmap+MAT等工具精准定位瓶颈,避免误判元空间或直接内存泄漏。

JVM 垃圾回收调优不是调几个参数就完事,关键在于理解应用行为、识别真实瓶颈、避免“凭感觉”乱改。很多问题其实出在误判 GC 类型、忽略元空间或直接内存泄漏、盲目追求低停顿却牺牲吞吐量等常见误区上。
别一上来就改 - 先看清楚到底是不是 GC 问题
很多“卡顿”“内存飙升”根本不是 GC 引起的。先确认是否真由 GC 导致:
- 用 -XX:+PrintGCDetails -XX:+PrintGCDateStamps 打印原始日志,观察 GC 频率、耗时、前后堆大小变化,而不是只看 GC 次数
- 用 jstat -gc
实时查 Eden/Survivor/Old/Metaspace 使用趋势,重点关注 Old 区是否持续上涨且不回收 - 用 jmap -histo:live
或 MAT 分析堆快照,确认大对象、集合类长期持有、线程局部变量未清理等非 GC 问题
分清场景再选收集器 - G1 不是万能解药
G1 适合大堆(4G+)、低延迟敏感场景,但小堆或吞吐量优先的应用反而可能更慢:
- 堆 ≤ 2G:CMS 已淘汰,Parallel GC 往往更稳更快;默认开启即可,不必强切 G1
- 堆 4–16G、要求 STW ≤ 200ms:G1 是合理选择,但必须配 -XX:MaxGCPauseMillis=200 并配合足够 CPU 资源,否则 G1 会降级为高频率小回收
- 堆 ≥ 16G、延迟极端敏感:ZGC 或 Shenandoah 更合适,但需 JDK 11+/17+,且注意部分 JNI 调用或监控 agent 可能不兼容
元空间和直接内存常被忽视 - 它们不走 GC 流程
OutOfMemoryError: Metaspace 或 Direct buffer memory 错误,和堆 GC 完全无关,调 -Xmx/-XX:MaxGCPauseMillis 没用:
- Metaspace 溢出:检查动态生成类(如 Spring CGLIB、Groovy 脚本、大量反射代理),加 -XX:MaxMetaspaceSize=512m 限制并观察是否频繁 Full GC;启用 -XX:+TraceClassLoading -XX:+TraceClassUnloading 看类加载/卸载是否失衡
- Direct memory 溢出:Netty、NIO 文件读写、图片处理易触发,加 -XX:MaxDirectMemorySize=1g 显式控制,并检查 ByteBuf 是否漏 release
调参不是填空题 - 关键要验证效果是否符合预期
改了参数不验证,等于没调。每次变更后至少观察一个完整业务周期(比如高峰流量时段):
- 关注指标不能只看“GC 时间短”:同步对比 吞吐量(TPS)、P99 延迟、Old 区使用率趋势、Full GC 次数
- 避免“组合拳陷阱”:比如同时调大 -Xmx 和 -XX:MaxGCPauseMillis,G1 可能因目标过松而减少 GC,导致 Old 区缓慢堆积,数小时后爆发一次超长 Full GC
- 生产环境禁用实验性参数:像 -XX:+UseStringDeduplication(仅 G1)、-XX:+UnlockExperimentalVMOptions 下的选项,除非明确知道副作用且已充分测试


















