Java 线上故障排查实战:CPU 100%、OOM 与死锁定位全流程
线上出问题时最怕什么?不是 Bug 本身,而是“无从下手”:CPU 飙到 100%、内存告警、接口全部超时、服务假死……本文整理一套 Java 应用线上故障排查的标准操作流程:先看什么、用什么命令、每一步怎么解读输出,覆盖 CPU 100%、内存溢出、死锁三大高频事故,并附 Arthas 的快速上手。
一、排查总纲:先定位再处理
- 先看现象:报警来自哪(CPU/内存/GC/线程/接口超时),影响面多大(单实例还是全挂);
- 先保命:紧急时先重启/扩容/切流止损,保留现场(进程、堆 dump、GC 日志)后再慢慢分析;
- 按层定位:操作系统(top/free/iostat)→ JVM 进程(jps/jstat)→ 线程(jstack)→ 堆对象(jmap/MAT);
- 对症修复:找到问题代码后再改,切忌盲目调参。
二、CPU 100% 排查(最经典的实战场景)
现象:接口卡死、负载飙高,可能原因:死循环、频繁 Full GC(GC 线程吃 CPU)、锁竞争空转、正则回溯、序列化过重等。
# 第 1 步:找到 CPU 占用最高的进程(Linux)
top -c # 记下高 CPU 的 PID
# 第 2 步:查看该进程内哪个线程最忙(按 CPU 排序)
top -Hp <PID> # 记下最忙线程的十进制 TID
# 第 3 步:把十进制线程号转十六进制(jstack 里线程号是十六进制 nid)
printf '%x\n' <TID> # 例如 19744 -> 0x4d20
# 第 4 步:导出线程栈,搜索对应 nid 前后的栈帧
jstack <PID> > thread.log
grep -A 30 '0x4d20' thread.log # 重点看它在干什么:业务死循环 or GC 线程
两个高频结论:
- 栈顶是
com.xxx...MyService.doSomething→ 业务死循环/重计算,回去查代码里的 while、递归; - 大量线程卡在
VM Thread/ GC 日志显示频繁 Full GC → 内存问题导致的“GC 风暴”,转下面 OOM 排查。
用 jstat -gcutil <PID> 1000 10 每秒打印一次各区使用率与 GC 次数,能快速判断是不是在疯狂 GC。
三、内存溢出(OOM)排查
先给 JVM 预留“案发现场”:
# 启动参数里务必带上(否则 OOM 时没有堆快照可分析)
java -Xms4g -Xmx4g \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/app/logs/heap-$(date +%s).hprof \
-jar app.jar
OOM 常见类型:
| 异常信息 | 含义 | 常见原因 |
|---|---|---|
| Java heap space | 堆内存不足 | 大对象/集合无限增长、一次性加载全表 |
| GC overhead limit exceeded | GC 回收不掉还在不停 GC | 濒临 OOM 的典型前兆 |
| Metaspace | 元空间不足 | 动态生成类过多(反射/CGLIB/热部署) |
| Unable to create new native thread | 线程创建失败 | 线程数达到 OS 限制或内存耗尽 |
| Direct buffer memory | 堆外内存不足 | Netty/ByteBuffer 未释放 |
分析堆快照:
# 1. 快速看对象直方图(无需 dump,进程还活着时可用)
jmap -histo:live <PID> | head -30
# 2. 导出堆快照(进程活着且能承受停顿再执行,大堆可能长时间 STW)
jmap -dump:format=b,file=app.hprof <PID>
# 3. 用 MAT(Eclipse Memory Analyzer)打开 hprof:
# - Leak Suspects 报告:直接告诉你“什么对象占了多少、被谁持有”
# - Dominator Tree(支配树):看内存大头与引用链
# - 定位到业务类:去代码里查 List/Map/cache 是否只增不减
判断是泄漏还是不足:多次 Full GC 后老年代占用持续上升、GC 后不回落 = 泄漏(查引用链);稳定高水位只在业务峰值出现 = 容量不足(评估是否扩容 + 检查是否有可省内存的写法)。静态集合、ThreadLocal 忘记 remove、连接未关闭、缓存无限增长是四类最常见泄漏源。
四、死锁排查
jstack <PID> | grep -A 20 'deadlock'
# 输出示例:
# Found one Java-level deadlock:
# =============================
# "thread-A": waiting to lock Monitor 0x... (object 0x...)
# "thread-B": waiting to lock Monitor 0x...
jstack 输出里出现 Found one Java-level deadlock 就直接给了环路的两个线程和它们各自持有的锁,定位到代码里加锁顺序不一致的地方即可。若没检测到但服务假死,看大量线程是否同时卡在 WAITING (on object monitor) 或 Locked ownable synchronizers,那通常是锁竞争或连接池耗尽(数据库连接、HTTP 连接池被占满),配合 Arthas 的 thread 命令查看线程状态分布。
五、Arthas 一把梭(强烈推荐)
Arthas 是阿里开源的 Java 诊断利器,无需重启、无需改代码即可在线观测,线上排查效率翻倍:
# 下载启动(附到目标进程)
java -jar arthas-boot.jar # 按提示选择 PID
# 常用命令速查
dashboard # 总览:线程、内存、GC 一览
thread -n 3 # 打印 CPU 占用最高的 3 个线程
thread -b # 找出阻塞其他线程的死锁(比 jstack 更直观)
thread --state WAITING # 看等待中的线程都卡在哪
jvm # JVM 信息速览(加载类数、线程数、内存)
memory # 内存区域使用情况(含堆外)
jad com.demo.OrderServiceImpl # 线上反编译,确认跑的代码和你想的是否一致
watch com.demo.OrderService create 'params[0]' 'returnObj' -x 2 # 观察入参出参
trace com.demo.OrderService create # 方法内耗时火焰分布,秒杀慢接口
monitor -c 5 com.demo.OrderService create # 每 5 秒统计调用次数与成功率
典型慢接口排查:trace 定位到最耗时的子调用(SQL?RPC?序列化?),再结合数据库慢日志和链路追踪(SkyWalking/Zipkin)确认根因。
六、故障排查命令速查表
| 命令 | 用途 |
|---|---|
| jps | 列出 Java 进程 PID |
| jstat -gcutil PID 1000 | 每秒看各内存区使用率与 GC 次数 |
| jstack PID | 线程快照:死锁、阻塞、CPU 忙的线程栈 |
| jmap -histo PID / -dump | 堆对象直方图 / 导出堆快照 |
| jinfo PID | 查看 JVM 启动参数与系统属性 |
| top / top -Hp / vmstat / free | OS 层:CPU、线程、内存、IO |
| Arthas:thread/trace/watch/dashboard | 在线诊断,无需重启 |
七、排查心法小结
- 一切排查的前提是启动参数带全日志与 dump 开关,没现场一切白搭;
- 按“OS → JVM → 线程 → 堆”逐层缩小范围,别一上来就 dump;
- CPU 高先想死循环和 GC,内存高先想泄漏和容量,卡死不响先想锁与连接池;
- 修完加监控与告警(GC 次数、线程池活跃度、接口 TP99),让下次问题在爆发前被发现。
工具只是放大器,真正值钱的是排查思路:先定性(哪一层)再定位(哪个线程/哪行代码),最后才是修复。