面向 P6-P7+ 工程师 进阶排障 · 工具链手册 约 9,000–11,000 字 信息截止 2026-08

线上 JVM 高频故障定位:
CPU / 泄漏 / OOM / 死锁工具链手册

这不是「见告警就重启」的速查单,而是值班窗口内可执行的排障判断力:先分拣现象、再选工具、再判读证据。 掌握四类高频故障的标准链路与命令判读要点,才能在告警并发时收敛根因,而不是在日志与线程栈之间反复试错。

主线风格:问题现场 + 排障 SOP 版本假设:JDK 11+(jcmd / jstack / jmap;MAT 离线分析) 证据等级:官方工具文档优先,标注推断与生产经验
问题现场 · 复合场景

1四条告警同时亮:先采样还是先重启

复合场景 · 综合多家金融核心交易值班复盘的典型模式,非指代具体单一事故 TYPICAL SCENARIO

某金融核心交易服务在 JDK 11 上运行,G1 为默认收集器。某次发布后的周三晚高峰,监控同时触发四条告警: 单 Pod CPU 持续 95% 以上、堆内存使用率 6 小时内从 45% 线性爬升到 92%、 order-service 进程被 OutOfMemoryError: Java heap space 杀死重启、 以及下游超时日志里出现 Deadlock detected 字样。

值班同学先后执行了重启、扩容、回滚,问题在 40 分钟内反复出现——因为四条告警背后可能是同一根因 (例如死循环线程既吃 CPU 又撑爆堆),也可能是四个独立问题叠加。没有固定框架时,团队往往在 「先查 GC 日志还是先 jstack」上争论,错过最佳采样窗口。

事后复盘(仍为复合推演)显示:若按本文 SOP 在第一次 CPU 告警时完成 top + jstack, 约数分钟即可指向 RiskRuleEngine 全量扫描;若在同一窗口做 histo diff,可提前发现 SessionContext 暴涨;若在超时告警时先落盘 jstack,deadlock 段可直接暴露对账与订单服务的 锁顺序反转。三者串联后,根因链为「发布引入全规则扫描 + ThreadLocal 清理遗漏 + 新增对账锁顺序不一致」——并非四个无关故障。

本文要交付的,就是让值班同学在第一次告警起就有序采样,而不是在第四次 OOM 后才拼凑片段。 假设读者已了解 JVM 内存区域与 GC 基础(同系列 T08),聚焦线上排障不变量:每类故障的 「现象 → 工具 → 判读 → 处置 → 预防」链路。风格上约 60% 框架训练(工具链与命令模板)、40% 问题现场(合成告警与输出解读)。

为什么要把四类故障放在一篇里讲?因为在真实值班中,告警往往是并发到达的:APM 报 CPU、Prometheus 报堆使用率、 日志系统报 OOM、网关报超时。若每类故障各有一套「口口相传」的排查习惯,新人容易在压力下选错入口——例如把 GC 占 CPU 当成业务死循环去改代码,或对 Metaspace OOM 一味调大 -Xmx。统一框架的价值在于:先用约三十秒做 现象分拣,再进入对应 SOP,且知道何时需要交叉验证(CPU 高 + 堆涨 → 先 jstack 再看是否有人在疯狂分配对象)。

排障不是纯技术动作。变更关联往往比多采三次 jstack 更快指向根因:最近发布、配置中心变更、流量切换记录, 应在 T+0~2 分钟与 PID 一并确认。本文所有命令与输出片段均可在 JDK 11 HotSpot 上复现;涉及容器与 K8s 的部分为机制共识与作者经验总结, 具体权限与镜像差异需在团队 Runbook 中固化。文中的金融交易场景为复合推演,用于串联四类故障,不代表单一真实事故全貌。

分享结构如下:第二章给出排障决策树与证据包模板;第三至六章分别展开 CPU、泄漏、OOM、死锁的命令与判读; 第七章保全四类 SOP 总表与跨类关联矩阵;第八章划定「适合深挖 / 不适合先分析」边界;第九章收束为决策矩阵与相邻知识地图。 机制描述属于机制共识,场景数字属于数量级经验,上线前必须用本服务 workload 复测。

常见误区

「重启最快止血」在证据未落盘时往往最贵:线程栈、堆快照与 GC 现场一并消失,复盘只能靠猜测。只有在确认已留存足够证据、或进程已不可恢复时才重启。另一个误区是「四条告警等于四个故障」——复合场景里更常见的是同一根因链的多表象,或两个独立问题叠加;分拣的目的不是否认并发,而是避免四条线各重启一次。

文中金融交易场景为复合场景,用于串联四类故障,不代表单一真实事故全貌。 命令与输出片段可在 JDK 11 HotSpot 上复现;容器与 K8s 部分为机制共识与作者经验总结,具体权限需在团队 Runbook 中固化。 读者若已在生产环境处理过单次 CPU 或单次 OOM,仍建议通读跨类关联与决策矩阵——真正拉开值班效率差距的,往往不是单个命令,而是多告警并发时的优先级判断。

体系总览 · 工具链地图

2排障不变量:先分拣,再选工具

线上 JVM 排障有三条不变量。先采样、后重启:重启抹掉线程栈、堆快照与 GC 现场。 现象分类先于工具选择:CPU、内存曲线、OOM 类型、线程阻塞是四条独立入口,但工具可复用 (jstack 同时服务 CPU 与死锁)。时间窗口意识:Full GC 前的 histo、OOM 后自动生成的 hprof、 CPU 尖峰后数分钟内的 jstack,价值远高于事后凭日志猜测。

图 1 · 四类高频故障统一排障决策树
自制示意图
告警 / 现象到达 现象分拣(30 秒) CPU 高 top -Hp → jstack nid 内存涨 jstat → histo → dump OOM 退出 按错误文案分支 超时 / 卡住 jstack 死锁段 热点栈 / GC 线程 MAT 引用链 堆 / 元空间 / Direct / 线程 锁顺序 / 池耗尽 修复 + 预防监控 + 复盘归档 原则:分类入口不同,证据包脚本可复用
读图要点:先分拣再进 SOP;多告警并发时用第八章关联矩阵决定优先级,避免四条线并行重启。

15 分钟值班时间线

以下时间线适用于复合场景级多告警并发,可根据实际裁剪。若团队有专职 SRE 与开发分工,建议 SRE 负责 T+0~5 分钟证据包与 top/jstack,开发负责 T+5 分钟起的栈帧与变更对照——边界清晰可避免多人同时重启。

  1. T+0~2 min:确认进程 PID、是否可 exec;拉取最近变更与流量曲线;禁止立即滚动重启。
  2. T+2~5 min:执行证据包(双 jstack、histo、jstat);top 找高 CPU TID;记录堆使用率与 FGC 计数。
  3. T+5~10 min:按四类现象分拣——CPU 与栈顶明确则同步开发止血;Old 涨且 histo 有 leader 类则评估 dump;deadlock 段落盘并通知开发。
  4. T+10~15 min:决策临时处置(限流 / 扩容 / 回滚)与是否 heap dump;若已 OOM 退出,收集 HeapDumpPath 与 gc.log。

排障前的环境检查清单

在敲第一条命令之前,确认以下事项可节省大量无效操作:

  • 权限:执行用户是否与 Java 进程属主一致,或具备 ptrace 能力;容器内常以 root exec,但某些加固镜像会禁止 jstack。
  • 工具完整性which jcmd jstack jmapjava -version 大版本一致;混用不同 JDK 版本的 jstack 连接目标进程可能失败。
  • 磁盘与 IO:heap dump 体积接近堆上限;数 GB 堆在 SSD 上 dump 也可能需要数分钟,期间 STW 或 IO 压力需与业务方同步。
  • 变更上下文:最近发布、配置中心变更、流量切换记录——排障不是纯技术动作,变更关联往往更快指向根因。

jcmd 常用子命令备忘

jcmd 是 JDK 11 起推荐的统一诊断入口,下列子命令在排障中出现频率最高(完整列表以 jcmd <pid> help 为准):

  • VM.flags:打印 JVM 启动参数,确认堆、Metaspace、OOM 相关 flags 是否与预期一致。
  • VM.system_properties:查看系统属性,排查误配的 MaxDirectMemorySize 等。
  • GC.heap_info:堆配置与各区容量,无需触发 GC。
  • GC.class_histogram:等价于轻量 histo,优先于 jmap -histo 以减少工具进程开销。
  • GC.heap_dump <path>:在线 dump;路径需在 Java 进程可写目录。
  • Thread.print:线程栈与死锁检测,等价 jstack。
  • VM.classloader_stats:类加载统计,Metaspace OOM 时使用。

SafePoint 与采样偏差

jstack、jmap -histo:live 等工具需要线程进入 SafePoint 才能获取一致快照。在极高负载或部分线程长时间运行在 native 代码 (如 JNI、某些 NIO 路径)时,可能出现「jstack 卡住数秒」或个别线程栈缺失。这不是工具坏了,而是采样机制约束。 实践上:若 jstack 长时间无响应,先用 kill -3 <pid> 向进程发送 QUIT 信号,HotSpot 会把线程 dump 写到标准输出或 hs_err 同目录——该方式同样依赖 SafePoint,但在某些挂起场景下多一条退路。JDK 11 的 jcmd Thread.print 与 jstack 底层同源,择一即可,避免重复触发。

JDK 11+ 内置工具速览

工具作用典型场景注意事项
jcmd <pid> help统一入口不确定该用哪个工具JDK 11 起推荐优先
jstack / Thread.print线程栈、死锁检测CPU 高、响应慢、死锁连续采 3 次间隔约 5s
GC.class_histogram对象计数与占用怀疑泄漏、OOM 前-histo:live 会触发 Full GC
GC.heap_dump完整堆快照泄漏确认、OOM 后大堆 dump 耗磁盘与 IO
jstat -gcutilGC 与各区利用率内存爬升、频繁 GC配合 -t 看趋势
MAT引用链 / 支配树泄漏根因离线分析,与生产 PID 解耦
来源:Oracle JDK 工具参考 · jcmd / jstack / jmap / jstat(Java Troubleshooting Tools)。
已核验事实

jcmd Thread.printjstack 在 HotSpot 上同属 Serviceability 线程转储能力;JDK 11+ 生产环境优先统一用 jcmd,减少混用不同 JDK 版本诊断客户端的失败概率。官方工具参考亦将 jcmd 定位为统一诊断入口,适合在 Runbook 中写成唯一推荐命令前缀。

将 APM 与 Prometheus 指标与本文四类入口对齐,可缩短「看哪张大盘」的决策时间。建议在 Grafana 为每个 Java 服务固化四行摘要:CPU、Old 区、FGC 速率、线程 BLOCKED 计数,并与监控映射表对齐为 On-Call 第一眼视图。没有这四行,值班往往会在十余张面板之间来回切换,浪费前两分钟黄金采样窗口。

# 证据包模板(按团队规范落盘)
PID=$(pgrep -f 'java.*order-service' | head -1)
mkdir -p /tmp/jvm-evidence-$(date +%Y%m%d%H%M)
jcmd $PID Thread.print -l > /tmp/jvm-evidence-*/jstack-1.txt
sleep 5
jcmd $PID Thread.print -l > /tmp/jvm-evidence-*/jstack-2.txt
jcmd $PID GC.class_histogram > /tmp/jvm-evidence-*/histo.txt
jstat -gcutil $PID 1000 10 >> /tmp/jvm-evidence-*/gcutil.txt
生产实践

容器内 PID 1 往往就是 Java 进程;镜像若只有 JRE 而无完整 JDK,则 jstack/jcmd 不可用。生产应预置 -XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath,并与 T10 容器内存 limit 联动。

监控信号与工具映射

监控信号可能故障类型优先工具次要验证
进程 CPU 突增CPU 飙升 / GC 忙top -Hp + jstackjstat -gcutil
堆使用率单调涨泄漏或流量增jstat + histo diffheap dump + MAT
进程消失 + OOM 日志堆 / 元空间 / Direct保留 dump 与 gc.log按 OOM 文案分支
线程池打满 + 延迟升死锁 / 阻塞 / 慢依赖jstack -l链路追踪(T14)
核心机制 · CPU

3CPU 飙升:把 OS 线程映射到 Java 栈顶

复合场景中第一条告警是 CPU 95%+。常见根因分三类:纯 Java 热点(死循环、正则回溯、序列化)、 GC 线程忙(分配过快或堆过小)、以及 native 层(Netty 直接内存、压缩库)。 排查目标是把 OS 线程 ID 映射到 Java 栈顶帧。容器中还需区分进程内热点与 cgroup throttle—— 后者 jstack 仍显示 RUNNABLE,但 throttle 指标同步上涨,扩容 CPU limit 比改代码更有效。

CPU 排障的核心不变量是操作系统线程 ID 与 Java 线程 nid 的一一映射。很多值班笔记只写「jstack 看一下」, 却省略 top 找 TID 的步骤,结果在两百以上线程的栈里全文搜索业务关键字——效率极低且易漏掉 GC 线程。 下面四步应作为肌肉记忆:用 top -Hpps -Lp 按 CPU 排序记下 LWP/TID;转为十六进制后在 jstack 中搜 nid=0x...;阅读栈顶五到十帧区分业务与 GC;间隔约五秒再采一次判断是否稳定热点。

判读线程栈时,建议从上到下阅读栈顶五帧 + 线程名 + 线程池名。线程名若含业务语义(如 biz-worker、http-nio、GC task), 可快速过滤无关线程。若高 CPU 线程名为 Attach Listener 或 VM Thread,需警惕是否有人正在执行 jmap dump 或频繁 jstack—— 那是诊断行为本身带来的副作用,勿误判为业务热点。

图 2 · top TID 到 jstack nid 的映射链路
自制示意图
top -Hp 记下高 CPU TID printf %x 十进制→十六进制 nid=0x.... 在 jstack 中定位 栈顶 5~10 帧 业务 / GC / native 间隔约 5 秒采第二次:栈顶相同多为稳定热点;栈顶变化可能是短任务或自旋
肌肉记忆:省略 top 找 TID,在两百线程栈里全文搜业务关键字,效率极低且易漏掉 GC 线程。
# 1. 找高 CPU 线程(示例 TID=12345)
top -Hp 9876 -b -n 1 | head -20
printf "0x%x\n" 12345   # → 0x3039

# 2. 抓取线程栈并定位 nid
jstack -l 9876 | grep -A 30 "nid=0x3039"

# 3. 若怀疑 GC 占 CPU
jstat -gcutil 9876 1000 5

栈顶判读表

栈顶特征含义下一步
java.util.regex.Pattern 反复出现灾难性回溯或不当编译审查正则、预编译 Pattern、限制输入长度
业务轮询无 sleep / 死循环应用层热点代码修复 + 边界测试
ParallelGC / G1 Concurrent MarkGC 线程占 CPU查分配速率、堆大小、Humongous
Unsafe / Netty 相关可能为 NIO 或 native限流;条件允许时用 profiler

复合场景:从 CPU 告警到代码行

order-service 执行 top -Hp,TID 28441 占用 78% CPU。 printf "%x\n" 28441 得到 0x6f19,jstack 定位到:

"biz-worker-12" #45 daemon prio=5 os_prio=0 cpu=78200.5ms elapsed=1200.3s
   java.lang.Thread.State: RUNNABLE
    at com.example.order.RiskRuleEngine.evaluateAll(RiskRuleEngine.java:187)
    at com.example.order.RiskRuleEngine.lambda$batchScan$0(RiskRuleEngine.java:142)

连续三次采样栈顶均在 evaluateAll,且位于全量规则集遍历——发布说明提及「临时开启全规则扫描」。 闭环是:top 找线程 → jstack 找栈 → 对照变更。若栈顶是 GC task,则同一 CPU 告警应转入 GC 日志与分配速率分析。

踩坑

只采一次 jstack 易误判 JIT 毛刺;采样控制在三次以内。另:把容器 CPU throttle 当成 Java 热点——top 仍高,但根因是 limit 过低,需结合 container_cpu_cfs_throttled_seconds_total

作者解释

jstat 显示 GCT 短时间占比很高且 FGC 快速增加,CPU 高往往来自回收而非业务。Old 区同时上涨时,CPU SOP 与内存 SOP 应并行:jstack 排除死循环后立即做 histo diff。

GC 占 CPU 的判读补充

jstat -gcutil 显示 GCT(GC 总时间占比)在短时间内超过约 30%,且 FGC 计数快速增加,CPU 高往往来自回收而非业务。 JDK 11 G1 下需结合 GC 日志中的 Pause Young、Pause Mixed 与 concurrent cycle。若同时 Old 区持续上涨,则可能是 分配过快 + 泄漏叠加——此时 CPU SOP 与内存 SOP 并行:jstack 排除业务死循环后,立即做 histo diff。 另需警惕:把容器 CPU throttle 当成 Java CPU 高——若 cgroup 限 CPU 导致线程 runnable 排队,top 仍显示 Java 进程高占用, 但根因是 limit 过低,需结合 kubectl describe pod 与 throttle 指标判断。

核心机制 · 泄漏

4内存泄漏:趋势 → 画像 → MAT 引用链

复合场景中堆使用率 6 小时从 45% 线性涨到 92%,Full GC 后 Old 区不降或降幅极小——这是泄漏式曲线 (需与「流量上涨导致合理缓存膨胀」区分:后者 Full GC 后应能回落,除非缓存无上限)。 切忌一上来就 dump:大堆 dump 对线上 IO 与停顿不友好,应严格遵循三阶段。内存类故障的排查纪律是: 趋势 → 画像 → dump,前一阶段证据不足时不进入下一阶段。

jstat 字段速读(JDK 11 G1)

jstat -gcutil <pid> 1000 输出列含义(机制共识):S0/S1/E/O/M 为各区使用率百分比; YGC/YGCT 为 Young GC 次数与耗时;FGC/FGCT 为 Full GC(或 Mixed 中 STW 部分,视收集器而定);GCT 为累计 GC 时间。 泄漏判读时盯 O 列:在流量平稳阶段,若 O 在多次 YGC 后仍阶梯上升,泄漏概率高。 M 列持续涨则怀疑 Metaspace 类加载问题,与堆泄漏 SOP 不同,勿混用 MAT 堆分析。

三阶段确认泄漏的框架如下:第一阶段用 jstat 持续观察 Old 与 Metaspace 是否单调上升,以及 Young GC 后 Old 是否只升不降; 第二阶段用 GC.class_histogramjmap -histo 对比间隔约三十分钟的两次输出,看哪些类实例数与字节数异常增长; 第三阶段在 Old 区仍高时做 heap dump,用 MAT 的 Leak Suspects 与 Dominator Tree 找 GC Root 到泄漏对象的保留链。

图 3 · 内存泄漏三阶段确认流程
自制示意图
阶段 1 · 趋势 jstat 盯 O 列 Full GC 后 Old 不降? 流量平稳仍阶梯涨 阶段 2 · 画像 两次 histo diff 锁定异常增长类 避免 histo:live 误导 阶段 3 · 根因 heap dump + MAT Dominator / Path to Roots 静态集合 / ThreadLocal 前一阶段证据不足时,不进入下一阶段
纪律:histo 先锁类,dump 再锁字段,引用链指向代码模式——排障文档的职责是定位,不是替代 Code Review。
jcmd 9876 GC.class_histogram | head -30 > /tmp/histo-t0.txt
# 等待 30 分钟或一次业务高峰后
jcmd 9876 GC.class_histogram | head -30 > /tmp/histo-t1.txt
diff /tmp/histo-t0.txt /tmp/histo-t1.txt

# 生产慎用:确保磁盘空间 > 堆大小
jcmd 9876 GC.heap_dump /tmp/heap-$(date +%s).hprof
来源:Eclipse Memory Analyzer · Leak Suspects / Dominator Tree(eclipse.dev/mat)。

MAT 判读要点

将 heap dump 导入 MAT 后,推荐按固定顺序点击:Leak Suspects → Dominator Tree → Path to GC Roots(exclude weak/soft)。 Leak Suspects 是启发式报告,可能误报;Dominator Tree 按 Retained Heap 排序更可靠。 Path to GC Roots 时务必排除 weak/soft 引用,否则常见缓存结构会呈现「巨大但合理」的引用链,干扰判断。 对于 char[] / byte[] 占主导的 histo,应在 Dominator Tree 中向上找谁持有这些数组,而不是停留在数组类型本身。

  • Shallow vs Retained:泄漏看 Retained(释放该对象能回收的总量)。
  • 常见模式:无界 static Map、未关闭的流、未反注册监听器、线程池场景 ThreadLocal 未 remove
  • OQL 示例SELECT * FROM java.util.HashMap h WHERE h.size > 10000,快速定位超大 Map。

复合场景:histo 锁定 SessionContext

在同一复合场景中,值班同学在 CPU 排查并行执行 histo 对比:约三十分钟内 com.example.order.cache.SessionContext 实例数从约 1.2 万增至 48 万, Retained 估算(通过两次 histo 字节差)与 Old 区涨幅高度相关。heap dump 后 MAT Leak Suspects 报告指向: 一个 ConcurrentHashMap 实例占据约六成 retained heap,被 SessionContextHolder.staticMap 引用—— ThreadLocal 未清理叠加线程池复用导致 SessionContext 永久可达。 此路径体现框架训练不变量:histo 先锁类 → dump 再锁字段 → 引用链指向代码模式。 修复通常是 finally { SessionContextHolder.clear(); } 或改用带 TTL 的缓存——但排障文档的职责是定位,不是替代 Code Review。

特征倾向泄漏倾向合理缓存
Full GC 后 Old几乎不降明显回落或平台
流量恒定内存仍涨内存稳定
发布关联新版本后出现与促销流量同步
histo leader业务 DTO / ThreadLocal已知缓存且设上限
踩坑

过早使用 jmap -histo:live 会触发 Full GC,暂时压低 Old 区,让泄漏曲线「好看」一次,从而错过最佳 dump 时机。把「缓存设太大」当成泄漏也是常见误判——判断标准应是相同流量下 Old 是否仍单调涨。

核心机制 · OOM

5OOM 分型:错误信息第一行就是入口

OOM 不是单一问题——错误信息第一行即分类入口。JDK 11+ 不同 OOM 的处置与预防差异很大。 值班时务必保存完整异常栈与同一时段 gc.log 末尾;许多 OOM 栈顶仅为分配失败点,真正根因对象可能在更早的 histo 或 dump 中。

图 4 · OOM 类型分诊树
自制示意图
OutOfMemoryError 文案 Java heap space 泄漏 / 大对象 / 堆小 dump + MAT Metaspace 动态类 / 热部署 classloader_stats Direct buffer Netty / NIO NMT summary unable to create native thread ulimit / pids.limit 容器 exit 137 ≠ JVM 内部 OOM 先读 K8s Events / dmesg,再决定调堆还是调 limit(见 T10)
分型纪律:对 Metaspace OOM 调大 -Xmx 无效;对 Direct OOM 只看 histo 也会空手而归。
OOM 类型典型信息根因方向定位工具
Java 堆Java heap space泄漏、单次大对象、堆过小HeapDumpOnOOM + MAT
元空间Metaspace / Class space类加载过多、CGLib、热部署VM.classloader_stats
直接内存Direct buffer memoryNetty、allocateDirect 未释放NMT;Netty leak detector
无法创建线程unable to create new native thread线程爆炸、ulimit、pidsjstack 计数;ulimit -u
GC 开销GC overhead limit exceeded堆太小且回收无效GC 日志;修泄漏或调堆
来源:HotSpot 虚拟机服务能力与诊断指南 · jcmd 子命令(Diagnostic Tools);OOM 文案以运行时实际输出为准。
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/heapdump.hprof
-XX:+ExitOnOutOfMemoryError
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags

jcmd 9876 VM.metaspace
jcmd 9876 VM.classloader_stats | head -50
jcmd 9876 VM.native_memory summary scale=MB   # 需启动时开启 NMT

分类型处置要点

Java heap space:复合场景中进程退出前若留有 heapdump.hprof,优先 MAT 看支配对象;若无 dump,查是否单次大分配(导出、报表、未分页查询)。临时增大 -Xmx 在泄漏场景仅延迟死亡。

Metaspace:动态代理、脚本引擎、热加载常见。JDK 11 默认 Metaspace 不设上限(受限于本地内存),生产建议显式 -XX:MaxMetaspaceSize 以便 fail fast,而非拖垮整机。用 jcmd VM.classloader_stats 看哪个 ClassLoader 加载类数量异常。

Direct buffer:Netty、gRPC、NIO FileChannel 常见。-XX:MaxDirectMemorySize 与 Netty io.netty.maxDirectMemory 应对齐。NMT 有开销,建议故障复现时短期开启 summary。

unable to create new native thread:除 -Xss 过大外,更常见是每请求 new Thread 或容器 pids.limit;jstack 线程计数超过数千应高度警惕。

推荐 JVM 诊断参数模板

以下参数组合为作者经验总结,生产落地需经压测与 SRE 评审,不构成唯一正确配置。强调排障可追溯性: 没有 gc.log 与 heap dump,OOM SOP 的后半段无法执行。GC 日志建议带 time、uptime、level、tags,便于关联 STW 与 CPU 毛刺; OOM 留存配合 ExitOnOutOfMemoryError,避免僵尸进程;K8s 中 dump 路径应挂载 PVC 或 sidecar 上传; Metaspace 与 Direct 内存上限按服务显式设置,使溢出在可控边界内 fail fast。

容器环境中 Pod 被 OOMKilled(exit 137)但 JVM 未抛 Java OOM,往往是 cgroup memory limit 小于堆 + 元空间 + 直接内存 + 线程栈之和—— 这属于 T10 范畴,但排障时要先读 dmesg / K8s Events 区分内核杀进程JVM 内部 OOM。 验证层判读:heap space 下方若紧跟某业务 byte[] 分配栈,多为单次请求过大;若 MAT 显示大量小对象同一类,偏向泄漏。 Metaspace OOM 在多版本热加载测试环境常见,生产应关闭不当的 DevTools 或动态代理泛滥。

已核验事实

Java 8 起永久代被 Metaspace 取代,类元数据占用本地内存。堆参数调好了,并不能自动约束 Metaspace 与 Direct Memory——这是 OOM 分型存在的根本原因。

复合场景中 Java heap space 与内存线性上涨实为同一根因链:ThreadLocal 泄漏 → SessionContext 堆积 → Old 满 → Full GC 无效 → OOM。 若在 Old 区 75% 时已做 histo diff,往往能在 OOM 前锁定持有者——对应 SOP 预防列的「堆使用率阶梯告警」。

核心机制 · 死锁

6死锁与广泛阻塞:jstack 死锁段与手工环

死锁类故障的迷惑性在于:监控上 CPU 可能正常——blocked 线程不消耗 CPU,但线程池被占尽后, 新请求在队列或网关层超时,表象像「下游挂了」或「网络抖动」。除 HotSpot 自动输出的 Found one Java-level deadlock 外,还应熟悉手工判读环:从 BLOCKED 的 waiting to lock, 找到持有者,再递归直至成环。对 java.util.concurrent 包下的 AQS 锁,jstack 会显示 park 与 LockSupport, 死锁段不一定能自动识别,需结合 -l 参数打印的锁附加信息(locked <0x...>)手工分析。 JDK 11+ 亦可用 jcmd <pid> Thread.print,效果等同。若未自动检测到,搜索 blocked on、waiting to lock,手工绘制锁等待图。

线程状态速查

Thread.State常见含义是否指向死锁
RUNNABLE执行中或等待 CPU否(除非自旋锁)
BLOCKED等待进入 synchronized可能,需看锁持有者
WAITING / TIMED_WAITINGwait、park、join通常否,除非交叉 wait
大量 BLOCKED 同一 monitor锁竞争或池耗尽可能为「伪死锁」

复合场景:死锁与超时的连锁

在复合场景中,Deadlock detected 来自对账任务与订单更新争抢两把内置锁顺序相反: ReconciliationJob 先锁 accountLock 再锁 orderLock,而 OrderService 路径相反。 jstack 末尾 deadlock 段清晰给出环。修复长期方案是全局锁排序(按 accountId、orderId 字典序加锁)或改为单锁分区。 短期处置只能重启释放——再次强调必须先 jstack -l 落盘再重启,否则复盘无栈可追。 分布式锁(Redis/ZK)造成的「逻辑死锁」不会出现在 JVM deadlock 段,但 jstack 会显示大量线程等待同一业务锁类——勿与经典 Java 死锁混淆。

图 5 · 经典死锁:锁顺序反转形成等待环
自制示意图
Thread A · ReconciliationJob 持有 accountLock 等待 orderLock Thread B · OrderService 持有 orderLock 等待 accountLock A 等待 B 持有的锁 B 等待 A 持有的锁 → 环 修复:全局锁排序(字典序)或单锁分区;短期只能重启——必须先 jstack -l 落盘
复合场景对应:对账先锁 account 再锁 order,订单路径相反——jstack 末尾 deadlock 段可直接给出环。
jstack -l 9876 | tee /tmp/jstack-full.txt
grep -A 5 "deadlock" /tmp/jstack-full.txt
grep -E "BLOCKED|waiting to lock" /tmp/jstack-full.txt | head -40
grep "java.lang.Thread.State" /tmp/jstack-full.txt | sort | uniq -c | sort -rn
现象jstack 特征处置
经典死锁Found Java-level deadlock;互持等待统一加锁顺序;tryLock 超时
连接池耗尽大量 BLOCKED 在 getConnection调池、查慢 SQL、连接泄漏
线程池队列满CallerRuns / AbortPolicy 栈限流、扩容、异步拆分
分布式「逻辑死锁」无 JVM deadlock 段,大量等业务锁查 Redis/ZK/DB 锁等待视图
生产实践

数据库行锁导致的全局阻塞,jstack 显示线程停在 JDBC,末尾不会有 Java-level deadlock。勿把 TIMED_WAITING 的 Object.wait 与死锁混淆——重点看 BLOCKED 且锁持有者链条。

生产实践 · 排障 SOP

7四类故障排障 SOP 总表

以下表格可直接纳入值班手册:现象 → 工具链 → 根因定位 → 处置 → 预防。与上文框架一一对应,便于告警路由、复盘归档与培训考核使用。 On-Call 收到 JVM 相关告警后,先在故障类型列做匹配(允许多选),再按对应行顺序执行工具链; 若约 15 分钟内无法收敛,升级并附带已采集的证据包路径。表中「应急处置」均为争取修复窗口的手段,不可替代根因修复。 建议将本表与 Prometheus 告警名做一一映射(如 JVMHeapUsageHigh → 内存泄漏 SOP 第 2 步 histo diff),减少值班翻文档时间。 SOP 表中「预防机制」列应回流到架构评审与上线检查单,否则同一类故障会按季度重复出现。

使用方式再强调三点:第一,允许多选——复合告警时按关联矩阵决定优先级,而不是四条线并行重启; 第二,每执行一步就落盘,路径写进升级工单;第三,复盘时把「本可在第几步收敛」写进行动项,推动预防列真正落地。 培训考核可要求新人在预发环境完成一次证据包脚本实操,并能口述跨类关联矩阵中的三条高频组合。

故障类型 典型现象 排查工具链 / 命令 根因定位要点 应急处置 预防机制
CPU 飙升 单核或多核 80%+ 持续;延迟上升;load 高 top -Hp → TID 转 hex → jstack nid=;jstat 排除 GC 栈顶是否稳定;是否 GC 线程;是否同一业务方法 限流/熔断;确认死循环且无法热修时,择低峰重启并保留 jstack 正则与循环评审;CPU 按服务告警;压测覆盖热点;发布加强观测窗口
内存泄漏 Old 区单调涨;Full GC 后 Old 不降;响应渐慢 jstat 趋势 → GC.class_histogram 对比 → heap dump + MAT Dominator Tree 保留链;静态集合 / ThreadLocal / 监听器 扩容争取时间;dump 后滚动重启;回滚可疑发布 缓存上限;WeakReference;定期 heap 基线;泄漏检测 CI
OOM 进程退出;OutOfMemoryError;K8s Restart 增 错误类型分类 → heap/metaspace/NMT;HeapDumpOnOOM 文件 按类型:堆/元空间/直接内存/线程;区分 137 OOMKill 重启并保留 dump 与 gc.log;临时调大 limit 仅作缓冲 启动参数模板;容器 limit 与堆比例(T10);大对象导出限流
死锁 / 广泛阻塞 接口超时;线程池 active=max;jstack 末尾 deadlock jstack -lThread.print;线程状态统计 是否环状持锁;连接池/线程池是否耗尽伪装成死锁 重启释放锁(临时);无法 dump 且业务允许时才强杀 统一锁顺序;tryLock 超时;池大小与超时监控;避免嵌套锁

跨类故障关联矩阵

四类故障并非完全独立。下表帮助在「多告警同时亮」时决定优先跟进哪条 SOP:

组合信号常见单一根因建议优先 SOP
CPU 高 + 堆涨 + heap OOM死循环中创建对象 / 无限缓存写入CPU(jstack 栈顶)→ 内存验证
CPU 正常 + 超时 + deadlock 段经典死锁或锁顺序错误死锁 SOP
CPU 高 + 堆不涨 + FGC 少纯计算热点或 native 忙CPU SOP,考虑 profiler
堆涨 + CPU 正常 + 无 deadlock泄漏或合理缓存内存 SOP(jstat + histo)
Metaspace OOM + 刚发布新依赖动态代理 / 脚本OOM 元空间分支
作者解释

SOP 表中「预防机制」列应回流到架构评审与上线检查单,否则同一类故障会按季度重复出现。证据包保留:jstack/histo/gc.log 至少 7 天,heap dump 保留至事故复盘关闭。

把 SOP 嵌进告警与演练

表格本身不会缩短平均修复时间,真正缩短 MTTR 的是「告警名 → SOP 行 → 证据包路径」的硬连接。 建议在告警注解里直接写:优先执行本文第七章某行,并附上证据包脚本路径。季度演练时不要只注入单一故障, 而要刻意注入「CPU 高 + 堆涨」或「超时但 CPU 不高」这类组合,强迫值班同学使用跨类关联矩阵,而不是默认重启。 演练结束后,把「本可在第几步收敛」写成复盘行动项,并回写到预防列——例如缺少缓存上限就补代码评审清单, 缺少 HeapDumpOnOOM 就补启动参数模板。如此循环,SOP 才会从文档变成组织肌肉记忆。

另需注意:SOP 的「应急处置」列永远是临时手段。扩容、限流、重启可以争取窗口,但不能替代引用链修复或锁顺序统一。 复盘纪要里应显式区分「止血动作」与「根因动作」,避免团队把扩容成功误认为问题已关闭。 对金融与交易类系统,还应规定:任何涉及 dump 或重启的操作,必须在变更窗口或带业务方确认的紧急通道内执行, 并保留操作时间线,便于事后审计与同类问题二次对比。

适合 · 不适合

8什么时候该深挖,什么时候该先止血

工具链手册容易让人「工具上瘾」:明明进程已不可服务,仍坚持完整 MAT 分析。 反过来说,也有人把每次 CPU 毛刺都当成必须 dump 的泄漏。边界如下——适合与不适合不是价值判断,而是对可观测窗口与业务损失的权衡。

适合深挖的典型信号是:进程仍可 attach、曲线呈泄漏式或 deadlock 段已出现、需要复盘与预防回流、同类故障在近几次发布后重复出现。 此时花十几分钟做证据包与 MAT,成本通常低于反复重启却找不到根因。不适合「先分析后恢复」的典型信号是:进程反复 OOMKill 已无法 attach、 全站不可用且有明确回滚点、磁盘不足以容纳堆大小、镜像缺少诊断工具——此时应先止血或走平台侧采集,再离线分析已落盘的 dump。

适合深挖工具链

  • 进程仍可采样,且业务允许短暂停顿窗口
  • 曲线呈泄漏式或 deadlock 段已出现
  • 需要复盘与预防回流,而非仅恢复容量
  • 同类故障在近几次发布后重复出现

不适合「先分析后恢复」

  • 进程反复 OOMKill,已无法 attach
  • 全站不可用且有明确回滚点——先止血再离线分析 dump
  • 磁盘不足以容纳堆大小,强行 dump 会拖垮节点
  • 权限/镜像缺失导致工具不可用——应走平台侧采集,而不是空转
生产实践

SafePoint 约束:jstack、histo:live 等需要线程进入安全点。极高负载或长时间 native 时,可能「卡住数秒」。可用 kill -3 <pid> 作为退路(同样依赖 SafePoint)。勿在同一分钟内重复触发多次全量 dump。另:async-profiler 在部分金融环境禁止附加 agent,此时仍以 jstack 与 jstat 为基线工具,不要因为缺少火焰图就放弃采样。

也建议在架构评审里把「证据包是否可执行」列为上线检查项:镜像是否含诊断工具、HeapDumpPath 是否可写、磁盘是否预留、告警是否映射到 SOP 行。 缺一项,值班时就会被迫在压力下临时造轮子。工具链手册只有嵌进发布与值班流程,才从「好看的文档」变成「可执行的组织能力」。

决策框架 · 收尾

9决策矩阵与相邻知识地图

9.1 决策矩阵

现场信号 继续当前 SOP 切换 / 并行另一 SOP 选型式处置(临时)
CPU 高,栈顶稳定业务方法 CPU SOP 至代码行 若同时 Old 涨 → 并行 histo 限流 / 回滚发布
CPU 高,栈顶 GC 线程 转 GC 日志与分配速率 怀疑泄漏则进内存 SOP 扩容堆或 Pod(缓冲)
Old 单调涨,流量平稳 泄漏三阶段 已 OOM → 叠加 OOM 分型 扩容争取 dump 窗口
超时,CPU 不高,有 deadlock 段 死锁 SOP 无 deadlock 但池打满 → 池/慢 SQL 重启前必须落盘 jstack
exit 137,无 Java OOM 文案 先判 cgroup OOMKill 联动 T10 内存预算 调 limit / MaxRAMPercentage,勿盲目加 -Xmx

9.2 值班 Runbook 最小集成

  • 每服务预置证据包脚本:双 jstack + histo + jstat,输出到共享卷或 sidecar。
  • 告警分级:堆 > 80% 持续 30 分钟触发 histo diff;> 90% 触发 on-call;OOM 后 ticket 关联 dump 路径。
  • 发布后 1 小时为 JVM 观测加强窗口,CPU 与 Old 区对比发布前同期。
  • 季度演练:预发注入死循环、ThreadLocal 泄漏、小堆 OOM、交叉锁,考核约 20 分钟内定位。

9.3 相邻知识地图

  • T08 JVM 内存与 GC 选型:理解为何 Old 不降、GCT 占比高时该进 GC 还是进泄漏 SOP。
  • T10 容器化下 JVM 适配:区分 JVM OOM 与 cgroup OOMKill,校准堆与 limit 比例。
  • T14 全链路追踪:把线程阻塞与慢调用关联到具体 Span,补全「假超时」路径。
阶段性结论

四类故障共享证据包,但不共享入口。值班价值不在于记住更多命令,而在于缩短「从并发告警到正确 SOP」的路径。框架训练可迁移;现场数字必须用本服务复测。把决策矩阵贴在值班频道置顶,比再开一次工具培训更能减少无效重启。

9.4 带走什么

  • 排障顺序:采样留存 → 现象分类 → 选工具 → 判读 → 处置;避免无证据重启。
  • CPU 与死锁共享 jstack;内存泄漏与堆 OOM 共享 histo / dump / MAT;元空间与直接内存需专用命令。
  • 复合告警可能同源:死循环既占 CPU 又撑堆;线程池死锁既超时又像「CPU 不高但服务不可用」。
  • 容器场景务必区分 JVM OOM 与 cgroup OOMKill,并与 T10 参数治理联动。
  • 跨类故障关联矩阵用于多告警并发时的优先级判断,避免在 OOM 后才开始第一次 jstack。
  • 将 APM / Prometheus 指标与本文四类入口对齐,可缩短「看哪张大盘」的决策时间。

本篇以复合场景串联 CPU、泄漏、OOM、死锁,核心交付是工具链不变量 + 四类 SOP 总表 + 决策矩阵。 框架训练(约六成)体现在:十五分钟时间线、三阶段泄漏确认、OOM 分类型入口、jstack 与 top 的映射方法——这些不依赖具体业务包名,可迁移到任何 Java 服务。 问题现场(约四成)体现在:RiskRuleEngine 热点栈、SessionContext histo 暴涨、heap OOM 与泄漏因果链、对账与订单锁顺序反转——用于训练「看到输出能想到下一步」。 下一步请把证据包脚本与告警名映射写进团队 Runbook,并在预发完成一次四类故障演练——那一页纸与那次演练,才是这篇分享真正留下的东西。 若你只有三十分钟阅读时间,请优先带走:先采样后重启、现象分拣再选工具、四类 SOP 总表,以及 exit 137 与 Java OOM 的区分。其余章节作为值班时按图索骥的附录即可,不必一次背完。