本篇是「JVM 与性能调优系列」第 12 篇,系列收尾。
前面 11 篇你都看了,但真到线上还是乱。因为缺一套「先想清楚再动手」的方法论。这篇把 JVM 调优收成一张图、一张表,以后遇事照着走。
一、调优的第一原则:别调优
听起来反直觉,但最重要。
80% 的「性能问题」根本不是 JVM 参数问题 ,而是代码或架构问题:无脑全表查询、缓存击穿、N+1、线程池配错、同步阻塞。这些问题靠 -Xmx 翻倍是盖不住的,反而掩盖了病灶。
而且 JVM 默认值(尤其 JDK 9+ 的 G1 自适应)已经相当聪明。对新项目:先用默认 + G1/ZGC,跑压测看指标,真有瓶颈再动参数。调优是「验证假设」,不是「堆参数碰运气」。
二、问题分类决策树
遇事先归类,再动手:
- 吞吐不够 → 看 Parallel,或调大堆、查计算热点(火焰图)
- 延迟高(停顿长) → G1 调
MaxGCPauseMillis,或上 ZGC - 内存溢出 → 走第 10 篇 OOM 流程(先分 Java OOM / OS Kill)
- CPU 高 → 走第 11 篇(死循环 / 频繁 GC / 死锁)
- 启动慢 / 元空间涨 → 看类加载、
-XX:MaxMetaspaceSize
分类错了,工具就用错------比如 CPU 高是 GC 引起的,你却在代码里找死循环。
三、一套通用调优流程(闭环)
定目标 → 建基线 → 找瓶颈 → 改一处验一处 → 固化+告警
- 定目标:吞吐优先还是延迟优先?量化指标(如「P99 停顿 < 200ms,吞吐 > 98%」)
- 建基线:开 GC 日志 + 压测,统计当前吞吐量、平均/最大停顿、Full GC 频率
- 找瓶颈:用日志、火焰图、Arthas 定位最制约指标的那一环
- 改一处验一处:只动一个参数,压测对比基线。有效固化,无效回滚换假设
- 固化 + 告警:把参数写进部署配置,把关键指标(老年代、直接内存、线程数)接告警
四、参数速查表(按用途分组)
内存
-Xms / -Xmx 堆初始/最大(生产设相等)
-XX:MaxMetaspaceSize 元空间上限
-XX:MaxDirectMemorySize 直接内存上限
收集器
-XX:+UseG1GC G1(JDK9+ 默认)
-XX:+UseZGC ZGC(大堆低延迟)
-XX:+UseSerialGC / -XX:+UseParallelGC 朴素场景
G1 关键
-XX:MaxGCPauseMillis 停顿目标
-XX:InitiatingHeapOccupancyPercent 触发 Mixed GC 的老年代占用(默认45)
-XX:G1HeapRegionSize Region 大小
-XX:G1ReservePercent 预留防晋升失败(默认10)
ZGC 关键
-XX:SoftMaxHeapSize 软上限,空闲时归还 OS
诊断(强烈建议常开)
-Xlog:gc* 统一 GC 日志
-XX:+HeapDumpOnOutOfMemoryError 出 OOM 自动留堆快照
-XX:HeapDumpPath=/path
五、监控与告警清单
调完不是结束,是开始监控:
- 老年代使用率(持续 >80% 预警,防 Full GC)
- Full GC 频率 / 最大停顿(>1s 立刻查)
- 直接内存、线程数(防 OS Kill)
- SafePoint 耗时(过长说明代码里有长循环不进安全点)
工具链:Prometheus + Grafana 采集 JMX,Arthas 在线急救,火焰图(async-profiler)定位热点。
六、JDK 版本选择
- JDK 8 仍是很多公司的稳定基线,但 CMS 已移除、ZGC 不可用
- JDK 17 / 21(LTS) 是新建项目的甜点区:ZGC 成熟、G1 更强、长期支持、性能更好
- 升级有成本(依赖兼容、测试),但长远看,新收集器和新特性带来的稳定性收益通常远超迁移投入
七、系列总结
12 篇串成一条线:
内存模型(第1篇)→ 对象布局(2)→ GC 算法(3)→ 分代(4)
→ 收集器 Serial/Parallel(5)→ CMS(6)→ G1(7)→ ZGC(8)
→ GC 日志调优(9)→ OOM 排查(10)→ CPU/死锁(11)→ 方法论(12)
从「内存长什么样」到「怎么查、怎么调」,你现在有了完整的 JVM 坐标系。调优的本质从来不是背参数,而是理解原理、用数据驱动、闭环验证。
八、火焰图定位热点
GC 和内存都正常,但 CPU 高、吞吐上不去?问题在业务代码本身的热点 。这时用 async-profiler 抓火焰图:
bash
# 采样 60 秒 CPU 火焰图
asprof -d 60 -f flamegraph.html <pid>
火焰图里最宽的那一层就是最耗时的调用栈。常见「热点」:
- 正则
Pattern.compile写在循环里(每次重建) - JSON 序列化大对象、反射调用未缓存
- 无意义的深拷贝、重复计算
这些靠 JVM 参数永远调不好,只能改代码。火焰图是区分「JVM 问题」和「代码问题」的照妖镜。
九、常见调优误区清单
- 堆越大越好:超过 32G 指针压缩失效,对象头翻倍;且大堆 Full GC 更慢
- MaxGCPauseMillis 设极小值:反而频繁 GC、吞吐崩
- 只在出事时调:应在上线前压测定基线,而非半夜救火
- 照搬网上的「最优参数」:别人 64G 堆的参数套到你 4G 堆上必然翻车
- 忽视监控:调完不设告警,下次出问题又从零查起
记住:参数是手段,指标是目标,理解原理才是根本。
十、调优 Checklist 速查
一张贴墙清单,覆盖绝大多数场景:
- 先用默认 G1/ZGC 跑压测,确认真的有瓶颈
- 开 GC 日志 + HeapDumpOnOutOfMemoryError 留现场
- 吞吐问题 → 看 Parallel / 堆大小 / 计算热点
- 延迟问题 → 调 MaxGCPauseMillis / 上 ZGC
- OOM → 分 Java OOM / OS Kill,按第 10 篇流程
- CPU 高 → 定死循环/频繁GC/死锁,按第 11 篇流程
- 一次只动一个参数,前后压测对比
- 固化参数 + 接告警,形成闭环
十一、从 0 到 1 建一套 JVM 监控
没监控的调优是瞎子摸象。最小可行监控:
- 暴露指标 :
-XX:+UseG1GC配合 JMX(-Dcom.sun.management.jmxremote)或用 jmx_exporter - 采集:Prometheus 拉 JMX 指标(堆/各代/GC 次数耗时/线程数)
- 展示:Grafana 画「堆使用率、GC 停顿 P99、Full GC 频率」三张核心图
- 告警:老年代 >85%、Full GC >3 次/5 分、最大停顿 >1s 触发告警
- 急救:Arthas 常备,随时在线定位
这套建起来,你从「半夜被叫醒救火」变成「告警里提前看到趋势」,运维体验天差地别。
总结
JVM 调优第一原则「别调优」:先排除代码/架构问题,再用默认收集器跑压测。遇事走决策树归类,用「定目标→建基线→找瓶颈→改验→固化告警」闭环,一次只动一个参数。把本文的速查表和告警清单存好,下次线上抖动,照着走就不慌。