JVM调优思路

JVM 调优思路(从“瞎调参数”到“可验证的工程方法”)

这份文档讲的是方法论 + 常见场景的落地步骤。JVM 调优不是背参数,而是:先测、再定位、再改、再验证
默认以服务端 Java(Spring Boot / 微服务 / 容器)为主,兼顾 JDK8~JDK17 常见差异。


1. 先把“调优目标”说清楚(不然你永远在乱跑)

调优的目标通常就三类,选一个当主目标,别三心二意:

  1. 低延迟:P99/P999 响应时间稳定,GC 暂停可控
  2. 高吞吐:单位时间处理更多请求(允许一定暂停)
  3. 低成本:更少内存、更少 CPU、同样吞吐(尤其容器/云上)

经验:线上多数问题是“延迟毛刺”或“内存不可控”,吞吐反而不是第一位。


2. 调优的基本套路(强烈建议照这个来)

2.1 先做基线(Baseline)

你需要一组“现在”的数据,才能证明“你改完更好了”。至少要有:

  • 业务指标:QPS、P99、错误率、超时率
  • JVM 指标:GC 次数/暂停、堆使用、线程数、类加载数
  • 系统指标:CPU、Load、RSS、IO、网络

2.2 再定位“瓶颈类型”

JVM 调优常见瓶颈几乎都落在这几类:

  • GC 压力大(分配太快 / 回收太慢 / 老年代膨胀)
  • 堆外内存吃满(DirectBuffer / mmap / Netty)
  • 元空间泄漏(类加载器泄漏、动态代理生成太多类)
  • 线程太多(栈内存、上下文切换、锁竞争)
  • CPU 火焰图热点(对象创建过多、序列化、正则、JSON、反射等)

2.3 再做小步实验(一次只改一个点)

  • 一次改一个参数或一个代码点
  • 压测/回放同样的流量
  • 用同样的指标对比(别凭感觉)

3. 先把“证据”拿到:你需要哪些观测手段

3.1 必开:GC 日志(别调优还不让 JVM 说话)

  • JDK8 常用:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationStoppedTime -Xloggc:/path/gc.log
  • JDK9+(推荐写法)
-Xlog:gc*,safepoint:file=/path/gc.log:time,uptime,tags,level

GC 日志是调优的“黑匣子”。没有它,很多结论都是拍脑袋。

3.2 线上常用命令(不重启也能看很多)

  • 查看 JVM 参数/版本/命令行:
jcmd <pid> VM.version
jcmd <pid> VM.flags
jcmd <pid> VM.command_line
  • 查看堆/类/线程概况:
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_stats
jcmd <pid> Thread.print
jstat -gcutil <pid> 1000
  • 生成 dump(高危操作:注意磁盘、会 STW):
jcmd <pid> GC.heap_dump /tmp/heap.hprof
jcmd <pid> VM.native_memory summary   # 需要 -XX:NativeMemoryTracking=summary/detail

3.3 低开销性能分析(强烈推荐)

  • JFR(JDK11+ 非常好用):看分配热点、锁、线程、GC、IO
  • async-profiler:火焰图定位 CPU 热点/分配热点
  • Arthas:临时看热点方法、线程阻塞、Trace 调用链

4. JVM 里“真影响线上”的几个内存块(别只盯着堆)

  • Java Heap(堆):对象主要在这;你调的大部分参数都围绕它
  • Metaspace(元空间):类元数据;类加载器泄漏会炸它
  • Thread Stack(线程栈):线程多了就会占内存;-Xss 影响
  • Direct/Native(堆外):Netty/NIO/ByteBuffer/mmap;OOM 不一定是堆 OOM
  • Code Cache:JIT 编译后的代码缓存,满了会退化性能

结论:你看到“机器内存快满了”,不代表堆满;也可能是堆外、线程、元空间。


5. 选择 GC:别迷信“最牛”,要看你的目标

5.1 常见选择(JDK 版本相关)

  • Parallel GC:吞吐强,暂停长(适合离线/批处理)
  • G1 GC:通用型,暂停可控(现代服务端默认主力)
  • ZGC / Shenandoah:超低暂停(延迟敏感、堆很大时很香,但看 JDK 版本支持)

如果你是常规 Spring Boot 微服务:优先 G1
如果你是低延迟且堆很大:考虑 ZGC/Shenandoah(前提:JDK17+ 更稳)。


6. 调优最常见的“症状 -> 原因 -> 处理”

下面按线上最常见的几种痛点给出“排查路径 + 参数方向 + 代码方向”。


场景 A:Minor GC 很频繁,P99 抖动明显

症状

  • GC 日志里 Young GC(或 G1 的 Evacuation Pause)很密
  • CPU 有波动,吞吐受影响
  • jstat -gcutil 看 YGC 次数飞涨

常见原因

  • 对象分配速率太高(JSON、字符串拼接、List/Map 频繁创建)
  • 新生代太小,撑不住分配洪峰
  • 大对象直接进老年代(G1 的 humongous)

处理步骤

  1. 先算“分配速率”
    • 用 JFR/async-profiler 看 Allocation flamegraph
  2. 业务上减分配:复用 buffer、避免临时对象、优化序列化
  3. 参数上:扩大可用堆/新生代(优先保证整体堆合理)
    • JDK8(CMS/Parallel)时代常见:-Xms -Xmx 固定 + -XX:NewRatio
    • G1 时代不建议死抠 NewRatio,更建议给足堆、让 G1 自适应
  4. 如果是 humongous(大对象):
    • 优化大数组/大字符串/大 ByteBuffer 的生命周期
    • 让大对象别频繁创建(比如把大响应做流式输出)

场景 B:Full GC / Mixed GC 很慢,卡顿明显

症状

  • 请求出现“秒级/十秒级”暂停
  • GC 日志出现 Full GC 或 G1 Mixed Pause 很长
  • 老年代占用长期高位

常见原因

  • 老年代真的装不下了(泄漏或缓存不受控)
  • 晋升过快(Survivor 放不住,直接进老年代)
  • Reference(Soft/Weak/Phantom)处理开销大
  • G1:Region 回收跟不上 / Humongous 多

处理步骤

  1. 判断是“泄漏”还是“业务峰值
    • 堆使用曲线是否回不去?(回不去很像泄漏)
  2. Dump + 分析(MAT / YourKit / JProfiler)
    • 看 Dominator Tree、Top Consumers、GC Roots
  3. 如果不是泄漏:
    • 给足堆(最简单有效)
    • 调整 GC 目标(例如 G1 的 -XX:MaxGCPauseMillis 别设置得太激进)
  4. 如果是泄漏:
    • 修代码(缓存、静态集合、ThreadLocal、监听器、ClassLoader)
    • 绝大多数“调参数”救不了真正泄漏

场景 C:CPU 很高,但 GC 并不频繁

症状

  • CPU 常年 80%+,但 GC log 很平稳
  • QPS 上去后延迟爆炸

常见原因

  • 业务热点(序列化/反序列化、加解密、压缩、正则)
  • 锁竞争/线程切换(线程太多)
  • 线程池队列堆积导致上下游雪崩

处理步骤

  1. async-profiler 采 CPU 火焰图(最直接)
  2. 如果锁竞争:JFR 看 Monitor/Lock 事件
  3. JVM 参数通常不是主因,重点在:
    • 减少热点路径的对象创建与拷贝
    • 控制线程数,避免“堆线程解决一切”的幻觉
    • 让 IO 等待别变 CPU 忙等

场景 D:OOM 但堆没满(最容易误判)

典型报错

  • java.lang.OutOfMemoryError: Direct buffer memory
  • OutOfMemoryError: Metaspace
  • unable to create new native thread

处理思路

  • Direct OOM:看 Netty/NIO buffer;限制/监控 -XX:MaxDirectMemorySize
  • Metaspace OOM:检查动态生成类/反射代理;设置 -XX:MaxMetaspaceSize 只是止血
  • Native thread:线程数太多或 -Xss 太大;减少线程、调小栈

建议开启 NMT(需要重启):

-XX:NativeMemoryTracking=summary

然后:

jcmd <pid> VM.native_memory summary

7. 一套“可抄作业”的调优检查清单

7.1 启动参数(通用基线)

  • 固定堆:减少动态扩容抖动
-Xms4g -Xmx4g
  • 记录崩溃信息、OOM 自动 dump(注意磁盘)
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp
-XX:ErrorFile=/tmp/hs_err_pid%p.log
  • 打开 GC 日志(JDK9+ 推荐)
-Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags

7.2 容器/K8s 必看(很多人死在这里)

在容器里,别继续用“按物理机内存”的老思路,建议使用百分比参数(JDK10+ 更友好):

-XX:InitialRAMPercentage=50
-XX:MaxRAMPercentage=75

重点:容器内存限制太小,堆 + 堆外 + 线程栈 + 元空间 = 一起把 Pod 干掉。


8. G1 常用调参方向(别上来就堆一堆 flags)

你真的需要调 G1 参数的情况:

  • 你已经有 GC 日志和明确瓶颈
  • 你已经确认“加堆/修分配”解决不了

几个常见方向:

  • 目标暂停时间(别设太小,不然 G1 会很激进、吞吐掉)
-XX:MaxGCPauseMillis=200
  • 触发 Mixed 回收阈值(让老年代别拖太久)
-XX:InitiatingHeapOccupancyPercent=30
  • 并行/并发线程(通常交给 JVM 自己,除非你 CPU 很小或很大)
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=2

经验:G1 最有效的“参数”经常不是参数,而是 给足堆 + 降低分配率


9. 线上一次完整调优的“实战流程模板”

  1. 明确目标:比如 P99 < 200ms,Full GC 不能超过 200ms
  2. 开启/收集:GC 日志 + 指标(至少 1~3 天)
  3. 定位:
    • GC 问题?看暂停、频率、老年代趋势
    • CPU 问题?火焰图
    • 堆外问题?NMT/Direct 指标
  4. 提出假设:比如“分配率太高导致 YGC 频繁”
  5. 实验:
    • 先改代码(减少分配/减少大对象)
    • 再改参数(加堆、调 G1 阈值)
  6. 验证:压测/回放 + 对比基线
  7. 固化:把结论写进 runbook(别下次又从头踩坑)

10. 常见误区(踩了就会“越调越差”)

  • 只看堆,不看堆外/线程/元空间
  • GC 暂停长就疯狂调 MaxGCPauseMillis(会牺牲吞吐,甚至更抖)
  • 不做基线,改完只靠感觉
  • 一次改一堆参数,最后根本不知道哪个有效
  • 把泄漏当作“堆不够”(短期能活,长期必炸)
  • 容器里照搬物理机参数(OOMKilled + 还以为是 JVM 崩了)

11. 最小可用的“参数模板”(给你一个起点)

11.1 通用服务端(G1,JDK11/17)

-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp
-Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags

11.2 延迟敏感(ZGC,JDK17+,需要验证)

-Xms4g -Xmx4g
-XX:+UseZGC
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp
-Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags

注意:不同 JDK 版本对 GC 实现和默认值差异很大,别跨版本硬抄 flags。


12. 快速自检:你现在到底该从哪里下手?

  • 你看到 P99 抖动 + GC 日志有长暂停 → 先看 GC 类型/暂停来源(Young/Mixed/Full)
  • 你看到内存涨到顶不回落 → 先怀疑泄漏(dump 分析),别先加堆
  • 你看到 CPU 常年高 → 火焰图先上,别先调 GC
  • 你看到 OOM 但堆不大 → 查 Direct/Metaspace/线程,别只看 -Xmx

13. 附:常用命令速查

# 进程
jps -l

# GC/堆概况
jstat -gcutil <pid> 1000
jcmd <pid> GC.heap_info

# 线程栈
jstack <pid> | head -200
jcmd <pid> Thread.print > /tmp/threaddump.txt

# 导出堆 dump(可能 STW)
jcmd <pid> GC.heap_dump /tmp/heap.hprof

# NMT(需要启动时开 -XX:NativeMemoryTracking=summary)
jcmd <pid> VM.native_memory summary

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值