2781 字
约 9 分钟
19
S05-A06 · JVM 问题定位的归属判定:哪些现象不该进 JVM 排查
2024-12-31
2026-11-09

服务变慢,第一反应是重启;重启把现场清掉了,也把证据清掉了。但更常见的浪费是反向的:把不属于 JVM 的问题拖进 JVM 排查,采了两天栈和堆,结论是下游一个接口超时。

定位的第一步不是取证,而是归属判定:这个现象归 JVM 管吗?判定错,后面所有证据都在解释一个不存在的原因。


1. 归属四问,任一为"否"就先改道

问 判据 否 → 去哪
是否所有实例同时变慢 单实例异常→本机资源或该实例代码 单实例:查本机 CPU/磁盘/邻居
资源指标是否真的异常 CPU、内存、IO、网络是否有一项偏离基线 都正常:查等锁、等外部、池耗尽(§2)
现象是否可复现 固定条件能再次出现 不可复现:先补观测,别硬猜
时间是否与某次变更对齐 发布、配置、流量、依赖版本 对齐:先回滚验证,再谈深挖

四问全过,才值得动线程栈与堆。第 4 问最省钱:多数"JVM 疑难"其实是同一时间窗的一次发布或一次依赖升级。

2. 响应慢而 CPU 不高:只有三类

类型 判别证据 定位入口 常见误判
锁等待 多数线程 BLOCKED 且栈顶同一把锁 连续栈 + 锁持有者 当成慢 SQL
外部依赖慢 出站调用耗时、连接池排队 依赖侧指标 + 池指标 当成 GC
线程池耗尽 活跃线程打满、队列堆积、拒绝计数 池的队列长度与拒绝数 当成 CPU 不足

三类的共同点:CPU 看起来闲着,请求却排着队。区别在于排队发生在自己进程内的锁、别人家的服务、还是自己的池。

PID=$(jps -l | awk '/app\.jar/{print $1}')
# 归属判定只要三样:线程状态分布、池指标、出站耗时
jcmd $PID Thread.print > t1.txt; sleep 5; jcmd $PID Thread.print > t2.txt
grep -c 'java.lang.Thread.State: BLOCKED' t1.txt t2.txt
jcmd $PID Thread.print -l | grep -A3 'pool-' | head -40   # 池线程的栈顶

3. 证据按"要不要停"分档,先取零成本的

档 证据 是否停顿 典型耗时 判别力
零 指标、访问日志、线程栈 毫秒级安全点 秒 分状态、分实例
低 类直方图、JFR 录制 一次短暂 STW / 个位数百分比开销 分钟 分对象、分事件
中 堆快照 全堆 STW,与堆规模相关 分钟到十几分钟 分引用持有者
高 加参数重启、造复现环境 停机 小时 换条件做对照

纪律只有一条:能在不停机、不改配置的前提下拿到的证据,先全部拿完。反过来做的代价是把可逆排查升级成不可逆变更。

4. 线程栈读的是分布,不是单点

状态占比 含义 下一步
RUNNABLE 高且栈顶集中 真在算 火焰图看热点方法
RUNNABLE 高但栈顶分散 IO、系统调用、JNI 看 strace/perf,别只看 Java
BLOCKED 集中在一把锁 锁竞争 找持有者,看它卡在哪
WAITING/TIMED_WAITING 占多数 等队列、等超时、空转 查池配置与下游超时

RUNNABLE 不等于在工作:它只是"没在等 JVM 的锁"。线程在 socket 读上、在 native 方法里、在页错误里,都会显示 RUNNABLE。单次快照最容易骗人的地方就在这里——同一条栈可能只是恰巧路过。

# 连续采样看"是否停在同一行",单次快照没有这个信息
for i in 1 2 3 4 5; do jcmd $PID Thread.print > s$i.txt; sleep 2; done
for f in s*.txt; do grep -A6 'pool-1-thread' $f | grep 'at ' | head -1; done | sort | uniq -c

5. 火焰图与采样 profiler 的适用面

问题 采样 profiler 能答 不能答
CPU 热点方法 是 精确调用次数
等锁时长 用 wall-clock 模式可近似 锁的排队顺序
分配热点 是(alloc 模式,开销较高) 堆外分配细节
单次慢请求 样本太少,几乎测不到 该请求的完整路径

采样按周期抓栈,稀有事件抓不到。P99 毛刺类问题用它往往"看不到异常",这不是没有异常,是采样没命中。

6. 堆与堆外:dump 的时机与代价

动作 停顿 产物大小 什么时候值得做
GC.class_histogram 一次短暂 STW 文本,KB 级 想知道"哪类对象在涨"
GC.heap_dump 全堆 STW 约等于堆使用量 需要引用链与持有者
NMT(-XX:NativeMemoryTracking) 启动时开,几乎无额外停顿 文本 怀疑堆外/native 增长
RSS 与堆使用量对比 无 两个数字 快速判断"是不是堆外"

堆 dump 之前先回答两个问题:磁盘够不够(写满磁盘比 OOM 更麻烦),以及能不能接受这次停顿(大堆按秒到十几秒量级)。答案是否,就先用直方图与 RSS 对比。

7. 内存增长的三种形态

形态 特征 常见原因 判定动作
阶梯 某事件后抬升并保持 缓存预热、连接池扩容、类加载 对照事件时间窗
持续线性 与请求量同向、回收不回落 真泄漏:静态集合、监听器、ThreadLocal 两次直方图差值 + 引用链
锯齿抬高 每次都回收,但谷底越来越高 会话/缓存无上限、对象晋升 看回收后水位而非峰值
# 形态判定:间隔取两次直方图,看"哪些类的实例在净增"
jcmd $PID GC.class_histogram > h1.txt; sleep 300
jcmd $PID GC.class_histogram > h2.txt
diff <(sort -k2 h1.txt) <(sort -k2 h2.txt) | grep '^>' | sort -k3 -nr | head

锯齿形态最容易被误判成泄漏:峰值一直很高,但每次 GC 都能落回去。判断依据是回收后的谷底,不是峰值,也不是 GC 次数。GC 次数多只说明回收频繁,说明不了内存是否泄漏。

8. 容器环境下的陷阱

陷阱 表现 处置
-Xmx 与容器 limit 不匹配 堆没满,进程被 OOMKilled 堆 + 元空间 + 堆外 + 栈 < limit
未感知 cgroup 限制 JVM 按宿主机核数/内存算默认值 显式写 -Xmx、-XX:ActiveProcessorCount
堆外增长无监控 RSS 涨、堆平稳 NMT + RSS 曲线
被内核杀掉 无 hs_err、无 JVM 日志、退出码 137 查 dmesg/kubectl describe 的 OOMKilled
# 容器里先确认三件事:JVM 看到的核数、堆上限、容器 limit
java -XX:+PrintFlagsFinal -version | grep -E 'MaxHeapSize|ActiveProcessorCount'
cat /sys/fs/cgroup/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes
dmesg -T | grep -i 'killed process' | tail -5      # 有记录 = 内核杀的,不是 OOM 异常

容器里"OOM"是两个完全不同的东西:JVM 抛 OutOfMemoryError(有堆栈、有 hs_err),和内核 OOM Killer 杀进程(什么都没有,只有一个退出码)。把后者当 JVM 问题查,永远查不到。

9. 复现优先于猜测

现象类型 能否造出来 手段
流量型(并发升高后变慢) 多数能 压测复现目标并发与数据量
数据型(特定记录触发) 能 从日志取真实样本回放
依赖型(下游抖动) 部分能 注入延迟/错误,观察是否同形
状态型(跑几天才出现) 难 只能靠观测先抓证据

能复现的问题,定位成本降一个数量级:可以随便 dump、随便改参数、随便加日志。不能复现时,唯一正确的动作是先把观测补上(慢日志、耗时直方图、池指标),等下一次出现时证据已在手上。

10. 定位记录模板

现象:订单查询 P99 从 200ms 升到 1.8s(11-09 14:20 起)
范围:3 个实例全部;同期 QPS 未变;CPU 35%(基线 40%)
时间窗:11-09 14:20 – 15:05,与 14:15 的配置发布对齐
证据:线程栈 5 次采样;BLOCKED 78% 集中在 InventoryLock;持有者停在 socketRead
      连接池 active=200/200,队列 1.2k,拒绝计数 0→430
      GC 停顿正常(P99 < 50ms),堆使用平稳
结论:线程池被下游库存服务拖满,非 GC、非内存
待验证:下游 14:10 上线后 RT 从 30ms 到 900ms(对方确认)
处置:池隔离 + 超时下调;观察 30 分钟

模板的价值在"证据"与"结论"两栏必须一一对应:结论里出现的每个词,都要能在证据里指到出处。

11. 排错

现象 原因 处置
jps 看不到进程 不同用户或容器 namespace 进容器/换用户执行
栈里全是 RUNNABLE 但 CPU 低 卡在 IO 或 native 用 perf/strace 补
dump 文件比堆小很多 只 dump 了活对象,或 dump 失败 核对返回码与文件完整性
老年代不降 有强引用链 拿引用链回代码找静态字段
容器里堆没满却被杀 内核 OOM Killer 调 limit 或降堆外/元空间

12. 边界

情形 需要 JVM 级排查吗 说明
一次性流量尖峰 不需要 容量问题,扩或限流
外部依赖故障 不需要 查依赖侧与超时配置
明确的代码逻辑错误 不需要 单测与日志就够
单请求偶发超时 通常不需要 采样抓不到,先加耗时打点
长期缓慢劣化 需要 这才轮到堆、GC、线程

13. 自测

  1. 归属四问里,哪一问能省掉最多无效取证?
  2. 响应慢而 CPU 不高,三类的判别证据分别是什么?
  3. RUNNABLE 为什么不能等同于"在工作"?
  4. 判断内存是泄漏还是缓存增长,看峰值还是看回收后水位?
  5. 容器里"退出码 137 且无 hs_err"指向什么?
  6. 结论栏与证据栏必须一一对应,这条纪律防止的是什么?
S05-A06 · JVM 问题定位的归属判定:哪些现象不该进 JVM 排查
https://www.clxhxhhr.top/posts/6175/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。