1四条告警同时亮:先采样还是先重启
某金融核心交易服务在 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,价值远高于事后凭日志猜测。
15 分钟值班时间线
以下时间线适用于复合场景级多告警并发,可根据实际裁剪。若团队有专职 SRE 与开发分工,建议 SRE 负责 T+0~5 分钟证据包与 top/jstack,开发负责 T+5 分钟起的栈帧与变更对照——边界清晰可避免多人同时重启。
- T+0~2 min:确认进程 PID、是否可 exec;拉取最近变更与流量曲线;禁止立即滚动重启。
- T+2~5 min:执行证据包(双 jstack、histo、jstat);top 找高 CPU TID;记录堆使用率与 FGC 计数。
- T+5~10 min:按四类现象分拣——CPU 与栈顶明确则同步开发止血;Old 涨且 histo 有 leader 类则评估 dump;deadlock 段落盘并通知开发。
- T+10~15 min:决策临时处置(限流 / 扩容 / 回滚)与是否 heap dump;若已 OOM 退出,收集 HeapDumpPath 与 gc.log。
排障前的环境检查清单
在敲第一条命令之前,确认以下事项可节省大量无效操作:
- 权限:执行用户是否与 Java 进程属主一致,或具备 ptrace 能力;容器内常以 root exec,但某些加固镜像会禁止 jstack。
- 工具完整性:
which jcmd jstack jmap与java -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 -gcutil | GC 与各区利用率 | 内存爬升、频繁 GC | 配合 -t 看趋势 |
| MAT | 引用链 / 支配树 | 泄漏根因 | 离线分析,与生产 PID 解耦 |
jcmd Thread.print 与 jstack 在 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 + jstack | jstat -gcutil |
| 堆使用率单调涨 | 泄漏或流量增 | jstat + histo diff | heap dump + MAT |
| 进程消失 + OOM 日志 | 堆 / 元空间 / Direct | 保留 dump 与 gc.log | 按 OOM 文案分支 |
| 线程池打满 + 延迟升 | 死锁 / 阻塞 / 慢依赖 | jstack -l | 链路追踪(T14) |
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 -Hp 或 ps -Lp 按 CPU 排序记下 LWP/TID;转为十六进制后在 jstack 中搜
nid=0x...;阅读栈顶五到十帧区分业务与 GC;间隔约五秒再采一次判断是否稳定热点。
判读线程栈时,建议从上到下阅读栈顶五帧 + 线程名 + 线程池名。线程名若含业务语义(如 biz-worker、http-nio、GC task), 可快速过滤无关线程。若高 CPU 线程名为 Attach Listener 或 VM Thread,需警惕是否有人正在执行 jmap dump 或频繁 jstack—— 那是诊断行为本身带来的副作用,勿误判为业务热点。
# 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 Mark | GC 线程占 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_histogram 或 jmap -histo 对比间隔约三十分钟的两次输出,看哪些类实例数与字节数异常增长;
第三阶段在 Old 区仍高时做 heap dump,用 MAT 的 Leak Suspects 与 Dominator Tree 找 GC Root 到泄漏对象的保留链。
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
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 是否仍单调涨。
5OOM 分型:错误信息第一行就是入口
OOM 不是单一问题——错误信息第一行即分类入口。JDK 11+ 不同 OOM 的处置与预防差异很大。 值班时务必保存完整异常栈与同一时段 gc.log 末尾;许多 OOM 栈顶仅为分配失败点,真正根因对象可能在更早的 histo 或 dump 中。
-Xmx 无效;对 Direct OOM 只看 histo 也会空手而归。| OOM 类型 | 典型信息 | 根因方向 | 定位工具 |
|---|---|---|---|
| Java 堆 | Java heap space | 泄漏、单次大对象、堆过小 | HeapDumpOnOOM + MAT |
| 元空间 | Metaspace / Class space | 类加载过多、CGLib、热部署 | VM.classloader_stats |
| 直接内存 | Direct buffer memory | Netty、allocateDirect 未释放 | NMT;Netty leak detector |
| 无法创建线程 | unable to create new native thread | 线程爆炸、ulimit、pids | jstack 计数;ulimit -u |
| GC 开销 | GC overhead limit exceeded | 堆太小且回收无效 | GC 日志;修泄漏或调堆 |
-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_WAITING | wait、park、join | 通常否,除非交叉 wait |
| 大量 BLOCKED 同一 monitor | 锁竞争或池耗尽 | 可能为「伪死锁」 |
复合场景:死锁与超时的连锁
在复合场景中,Deadlock detected 来自对账任务与订单更新争抢两把内置锁顺序相反:
ReconciliationJob 先锁 accountLock 再锁 orderLock,而 OrderService 路径相反。
jstack 末尾 deadlock 段清晰给出环。修复长期方案是全局锁排序(按 accountId、orderId 字典序加锁)或改为单锁分区。
短期处置只能重启释放——再次强调必须先 jstack -l 落盘再重启,否则复盘无栈可追。
分布式锁(Redis/ZK)造成的「逻辑死锁」不会出现在 JVM deadlock 段,但 jstack 会显示大量线程等待同一业务锁类——勿与经典 Java 死锁混淆。
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 且锁持有者链条。
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 -l 或 Thread.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 的区分。其余章节作为值班时按图索骥的附录即可,不必一次背完。