← Java

JVM 排障:先区分 CPU、等待和内存,再决定改哪个参数

以 Java 17 为边界,建立从服务症状到线程、JFR、GC 和原生内存的证据链,避免凭感觉调堆。

Read in English →

工程学习与设计笔记。示例使用合成场景;适用版本与参考资料见正文。

适用范围:HotSpot JDK 17,GC 部分以 G1 为例。命令里的 12345 是示例进程号,执行前应替换;容器环境还要核对 PID 命名空间与工具权限。

“接口变慢了,先把堆调大”是一种成本很高的猜测。慢可能来自线程排队、锁竞争、网络、CPU 节流,也可能来自 GC。排障的第一步,是把这些候选解释分开,而不是先决定答案。

先保存同一个时间窗口的证据

记录异常开始时间、部署版本、请求量、错误率、延迟分位数与依赖耗时,同时看进程 CPU、内存、容器限制和磁盘。若只保存一张 GC 图,而接口图与机器图来自另一时段,很容易建立错误的因果关系。

然后确认运行环境:

jcmd 12345 VM.version
jcmd 12345 VM.flags
jcmd 12345 GC.heap_info
jcmd 12345 Thread.print -l

诊断命令支持情况可通过 jcmd 12345 help 查看,影响等级也应在执行前确认。官方定义见 JDK 17 jcmd 。不要把开发机命令能跑,等同于生产容器里一定可用。

CPU 高时,找出时间花在哪里

CPU 高既可能是业务计算增加,也可能是序列化、正则、忙等或 GC。线程栈能提供位置线索,但单次快照容易碰巧停在无关代码。连续取样时若同一段栈持续出现,再结合 CPU 采样,才有更强的解释力。

可以在允许的诊断窗口启动短时 JFR:

jcmd 12345 JFR.start name=incident settings=profile duration=60s filename=/tmp/incident.jfr

观察热点栈、锁、分配和 I/O 事件,再回到服务请求链路解释。profile 配置有额外开销,应先在同类环境评估;文件应放在容量充足且访问受控的位置。JFR 的能力与使用方式见 JDK 诊断工具指南 。

如果进程 CPU 看起来不高,但容器遭到 CPU throttling,应用仍可能缺少实际执行时间。此时增加线程只会让竞争更严重。应把进程指标与宿主机、容器配额放在一起看。

CPU 不高,延迟却很高

这类情况优先检查等待。线程是在等数据库连接、HTTP 连接、锁、队列,还是某个永远没有完成的 future?再看对应资源的使用量与等待时长。

例如工作线程都在等待连接池,而连接池中的连接又长时间执行慢 SQL,那么把工作线程从 32 调到 128,并不会增加数据库的完成速度,只会增加等待者。修复应指向真正稀缺的资源:慢查询、连接持有时间或下游容量。

也要检查线程池任务之间是否形成互相等待。一个任务把子任务提交到同一已满线程池,再同步等待子任务完成,可能造成线程饥饿。排障时画出“谁等待谁”的关系,比统计 WAITING 状态数量更有用。

内存上升不自动等于堆泄漏

堆在一次 GC 周期内上升很正常。更值得关注的是相似流量下,GC 后的存活量是否持续增长,以及哪些对象被长期保留。缓存没有上限、队列没有边界、监听器没有注销,都会造成符合代码逻辑却不符合容量预期的增长。

进程 RSS 还包含直接内存、线程栈、元空间与其他原生开销。若堆平稳但进程持续膨胀,应向这些方向扩展调查。启用 Native Memory Tracking 的进程可以比较基线与后续差异;它需要启动时配置,也不覆盖所有第三方原生分配,不能当作完整的 RSS 账本。

Heap dump 有助于分析对象保留链,但可能带来暂停、磁盘占用,并包含业务数据。应在明确问题需要它、容量足够的情况下采集。先用低成本观测缩小范围,再拿大文件验证假设,通常更有效。

GC 参数应该服务于已经确认的问题

对 G1,先记录暂停原因、频率、分配速度、GC 后存活量与大对象情况。暂停目标是调节目标,不是硬性延迟保证。官方建议从默认设置与合理堆上限出发,再针对实际现象调优,参见 G1 Tuning 。

每轮只改变一个主要变量,在可比较负载下观察吞吐、尾延迟、CPU 与内存。若把堆、GC 参数、线程池和连接池一起改了,即使指标变好,也很难知道哪一项有效,更难判断下一次故障应该怎么处理。

排障记录最后应能连成一句话:什么症状发生在什么时间,哪些证据排除了其他解释,哪个改动改变了哪个机制,以及回归验证是否支持这个结论。

References

← 继续阅读专题回到顶部 ↑