爪爪哇碎碎念

JVM 调优检查表

线上出问题时最容易慌,其实只有三类故障。照着顺序走,别一上来就改参数。

A4 可打印 JDK 8 / 17 / 21 通用 命令可直接复制
先记住一句话:没有观测数据的调优,都是猜。 这份表的前半部分全是「怎么拿到数据」, 参数只在最后出现。

一、先分清是哪一类故障

现象大概率的根因第一条命令
CPU 100%,接口全部变慢死循环 / 正则回溯 / 频繁 GC / 线程数过多top -Hp <pid>
OOM,进程被杀或抛 OutOfMemoryError内存泄漏 / 堆太小 / 大对象一次性加载jcmd <pid> GC.heap_info
接口偶发卡顿 1~3 秒,CPU 不高Full GC 停顿 / 锁竞争 / 外部调用超时jstat -gcutil <pid> 1000
内存缓慢上涨,重启就好静态集合累积 / ThreadLocal 未清理 / 连接未关jcmd <pid> GC.class_histogram

二、8 步定位(按顺序做,别跳)

第 1 步:确认 JVM 真实参数

jcmd <pid> VM.flags
jcmd <pid> VM.system_properties | grep -E 'java.version|java.home'

容器里最常踩的坑:-Xmx 没设,JVM 按宿主机的内存算,被 OOMKilled 才发现。JDK 10+ 默认开 UseContainerSupport,JDK 8u191 之前不支持 cgroup 限制。

第 2 步:看 GC 是不是元凶

jstat -gcutil <pid> 1000 20

看两列就够:FGC(Full GC 次数)在一分钟内涨了,且 FGCT 跟着涨 → 是 GC 问题;O(老年代使用率)Full GC 后仍不下降 → 有对象一直被引用,是泄漏不是参数问题。

第 3 步:定位到线程(CPU 高时)

top -Hp <pid>              # 找出占用最高的线程 TID
printf '%x\n' <TID>       # 转成十六进制
jstack <pid> | grep -A 30 "nid=0x<hex>"

连续抓 3 次(间隔 5 秒),同一个栈顶重复出现才是真热点;只抓一次经常抓到 GC 线程或 JIT 编译线程。

第 4 步:确认是不是 GC 线程在烧 CPU

如果 top -Hp 里排前面的线程名是 GC Thread# 或 G1 Young RemSet,别再翻业务代码了,回到第 2 步。

第 5 步:看堆里到底是谁

jcmd <pid> GC.class_histogram | head -30

注意:这条会触发一次 STW(很短),生产可用但要挑时机。要找「实例数异常多」的类,不是「占内存最大」的类——byte[] 永远排第一,没意义。

第 6 步:需要现场就 dump,但要先看磁盘

df -h /tmp
jcmd <pid> GC.heap_dump /data/dump-$(date +%s).hprof

堆多大文件就多大。dump 期间会 STW,线上大堆可能几十秒,先摘流量再 dump。JDK 8 上用 jmap -dump:live,format=b,file=...。

第 7 步:低开销地看一段时间

jcmd <pid> JFR.start duration=120s filename=/tmp/app.jfr
# 用 JDK Mission Control 打开,看分配热点与锁竞争

JFR 开销通常低于 2%,比 async-profiler 更适合「先开着看」。JDK 8 需要 -XX:+UnlockCommercialFeatures,JDK 11+ 直接可用。

第 8 步:只有在 1~7 步都有数据之后,才动参数

改参数前把「改前 / 改后」的同一指标记录下来,否则你永远不知道是参数生效还是流量降了。

三、参数速查表

参数作用建议
-Xms = -Xmx避免堆反复伸缩生产必设相等
-XX:MaxRAMPercentage=70按容器限额算堆容器场景优于写死 -Xmx
-XX:+HeapDumpOnOutOfMemoryErrorOOM 时自动 dump必开
-XX:HeapDumpPathdump 落盘位置指向大磁盘,别放容器临时层
-Xlog:gc*:file=/var/log/gc.log:time,uptime:filecount=5,filesize=50MGC 日志(JDK 9+)必开,滚动防打满
-XX:+PrintGCDetails -Xloggc:GC 日志(JDK 8)JDK 8 用这组
-XX:MaxMetaspaceSize限制元空间建议设,防动态生成类撑爆
-XX:MaxGCPauseMillis=200G1 停顿目标别设太小(<50 会让堆变小、GC 更频繁)
-XX:ParallelGCThreads并行 GC 线程数容器里按 limit 核数算,不是宿主机核数
-XX:+ExitOnOutOfMemoryErrorOOM 直接退出让编排系统重建,比半死状态好

四、GC 选型判断

  • 堆 ≤ 4G,追求吞吐:Parallel。批处理、离线任务。
  • 堆 4G~32G,通用服务:G1。JDK 9 起是默认,够用且稳定。
  • 停顿要求 < 10ms,堆很大:ZGC(JDK 15+ 生产可用,JDK 21 有分代 ZGC)。代价是吞吐略低、内存占用略高。
  • 不要做的事:在生产上为了「看起来更快」把 G1 换成 ZGC 却不测吞吐;ZGC 的染色指针会吃掉一部分堆外内存,容器里要留余量。

五、五个最常见的坑

  • 容器里没设 -Xmx,JVM 按宿主机内存算 → 被内核 OOMKilled,日志里连 OOM 都没有。
  • 把「Full GC 后老年代不降」当成参数问题去调 G1 → 实际是内存泄漏,调参数只是延后爆炸。
  • 只看 top 的进程 CPU,不看 top -Hp 的线程 → 分不清是业务线程还是 GC 线程。
  • dump 直接打到容器临时层 → 容器一重启,现场没了。
  • GC 日志不滚动 → 磁盘写满,服务以另一种方式挂掉。

六、和这份表配套读的

这份表给的是排查顺序,不是结论。任何一次参数调整都必须有改前改后的同一指标对比, 否则下次出问题你还是不知道原因。