java 技术随笔

Java 线上故障排查实战:CPU 100%、OOM 与死锁定位全流程

线上出问题时最怕什么?不是 Bug 本身,而是“无从下手”:CPU 飙到 100%、内存告警、接口全部超时、服务假死……本文整理一套 Java 应用线上故障排查的标准操作流程:先看什么、用什么命令、每一步怎么解读输出,覆盖 CPU 100%、内存溢出、死锁三大高频事故,并附 Arthas 的快速上手。

一、排查总纲:先定位再处理

  1. 先看现象:报警来自哪(CPU/内存/GC/线程/接口超时),影响面多大(单实例还是全挂);
  2. 先保命:紧急时先重启/扩容/切流止损,保留现场(进程、堆 dump、GC 日志)后再慢慢分析;
  3. 按层定位:操作系统(top/free/iostat)→ JVM 进程(jps/jstat)→ 线程(jstack)→ 堆对象(jmap/MAT);
  4. 对症修复:找到问题代码后再改,切忌盲目调参。

二、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 exceededGC 回收不掉还在不停 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 / freeOS 层:CPU、线程、内存、IO
Arthas:thread/trace/watch/dashboard在线诊断,无需重启

七、排查心法小结

  • 一切排查的前提是启动参数带全日志与 dump 开关,没现场一切白搭;
  • 按“OS → JVM → 线程 → 堆”逐层缩小范围,别一上来就 dump;
  • CPU 高先想死循环和 GC,内存高先想泄漏和容量,卡死不响先想锁与连接池;
  • 修完加监控与告警(GC 次数、线程池活跃度、接口 TP99),让下次问题在爆发前被发现。

工具只是放大器,真正值钱的是排查思路:先定性(哪一层)再定位(哪个线程/哪行代码),最后才是修复

标签
线上排查ArthasOOM性能