☰
线上 Java 故障排查|CPU 飙高、线程死锁、OOM 溢出定位,Arthas 实战全套流程
2026/10/27 10:27:38 网站建设 项目流程

前言

前面三篇都是JVM理论知识,理论最终目的就是为了线上故障排查、JVM调优。 Java后端线上最常遇到三类噩梦级问题:

  1. 服务器CPU持续飙高,负载居高不下,接口响应很慢
  2. 线程死锁,服务卡住、接口卡死不返回,线程堆积
  3. OOM内存溢出,程序直接崩溃重启,频繁宕机

很多同学遇到线上问题只会看日志,日志没有报错就无从下手。 本篇分为两大部分:第一部分JDK自带工具(无需额外安装,服务器自带);第二部分Arthas阿里诊断神器实战;每一类故障都给一套标准化排查流程、命令、定位思路。

前置回顾:STW、GC、堆、虚拟机栈、线程栈信息,是看懂dump文件的基础。

一、前置说明:JDK自带工具包

在JDK/bin目录下,自带一系列诊断工具,不需要在服务里引入依赖,生产环境优先使用(很多公司禁止随便上传Arthas)。

工具作用
jps查看Java进程PID,最基础入口
jstatGC统计监控,看GC频率、耗时、新生代老年代变化
jstack打印线程栈快照,排查死锁、CPU高、线程阻塞
jmap堆快照dump,查看对象内存占用,排查OOM、内存泄漏
jhat解析hprof堆快照(弱,生产一般不用,改用MAT)
jvisualvm图形化监控工具,本地调试很方便

重要提醒:所有dump、jstack操作,都会触发短暂STW;线上不要频繁执行,拿到一次有效快照就先分析,不要反复打快照。

1.1 jps 先拿到进程PID(所有排查第一步)

# 列出所有java进程PID、主类名 jps -l # 输出示例 12568 com.xxx.Application 23410 org.apache.tomcat.startup.Bootstrap

拿到PID=12568,后续所有命令都带上这个PID。

二、场景1:CPU 100% 飙高排查(最高频)

排查思路

  1. top 命令找到占用CPU最高的Java进程PID
  2. top -H -p 进程PID,找到这个进程内部CPU最高的线程TID(十进制)
  3. 将十进制TID转为16进制
  4. jstack打印进程线程栈,根据16进制线程ID定位线程栈,看这段线程在执行什么代码
  5. 定位代码:死循环、大量计算、频繁GC、正则回溯、大循环等

完整实操命令

# 1、查看服务器整体CPU,找到java进程PID top # 2、查看该进程下所有线程CPU占用,找到高CPU线程tid(十进制) top -H -p 12568 # 3、十进制转16进制(例如tid=13002) printf "%x\n" 13002 # 输出:32ca # 4、导出线程栈 jstack 12568 > thread.txt # 在thread.txt中搜索 0x32ca,找到对应线程栈
典型高CPU栈场景
  • 看到大量业务代码循环逻辑:代码死循环、while(true)没有sleep、大数据量循环处理
  • 看到GC task thread:GC线程占满CPU →GC频繁,大概率内存泄漏,转去排查GC/OOM
  • 正则相关包:正则回溯灾难性回溯导致CPU跑满

Arthas方式快速定位CPU高(更简单)

# 启动arthas,attach到java进程 java -jar arthas-boot.jar # 命令:监控线程CPU,默认采样10次 thread -n 5

直接输出CPU最高的5个线程栈,一步到位,省去top转16进制的繁琐。

三、场景2:线程死锁、接口卡死、线程阻塞

死锁四个必要条件(面试必背)

  1. 互斥条件:资源只能一个线程持有
  2. 请求与保持:线程拿到一个锁,再去申请另外一个锁
  3. 不可剥夺:锁不能被其他线程强行抢走
  4. 循环等待:线程A拿锁1等锁2;线程B拿锁2等锁1

满足4条就会产生死锁,线程永久卡在那里,不会释放,连接池慢慢耗尽,所有接口卡住。

jstack排查死锁

jstack 12568 > thread.txt

打开文件,直接搜索关键字:Found one Java-level deadlockjstack会自动帮我们检测出Java锁死锁,输出:

  • 哪些线程
  • 各自持有的锁
  • 正在等待哪个锁
  • 对应代码行号

区分:死锁 vs 阻塞 死锁:线程互相等锁,永远不会自己恢复,必须重启

普通阻塞:等待数据库连接、http接口、锁竞争,外部资源恢复后可以自己解开

Arthas查看线程

# 查看所有线程,支持查看阻塞、死锁 thread -b

thread -b专门用来定位阻塞线程,找出持有锁的线程,快速定位锁等待链路。

模拟死锁代码(方便本地复现测试)

public class DeadLockDemo { private static final Object lockA = new Object(); private static final Object lockB = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lockA) { try { Thread.sleep(100); } catch (Exception e) {} synchronized (lockB) { System.out.println("线程1拿到两把锁"); } } }).start(); new Thread(() -> { synchronized (lockB) { try { Thread.sleep(100); } catch (Exception e) {} synchronized (lockA) { System.out.println("线程2拿到两把锁"); } } }).start(); } }

运行后jstack即可抓到死锁日志。

四、场景3:OOM内存溢出、内存泄漏(最复杂)

OOM的本质:堆内存耗尽,Full GC之后仍然没有足够内存分配新对象,JVM抛出OutOfMemoryError。

区分两个概念: OOM:内存用完了,直接报错崩溃 内存泄漏:对象本应该被回收,但持续被引用链持有,慢慢堆积,最终引发OOM

方案A:提前配置JVM参数,OOM自动dump(生产推荐)

在启动脚本增加参数:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/dump/oom.hprof

程序OOM崩溃瞬间,自动导出堆快照,保存现场,不会丢失证据。

注意:hprof文件大小约等于-Xmx最大堆,磁盘要预留足够空间。

方案B:手动导出堆快照(服务还没崩,只是内存持续上涨)

# jmap导出堆快照 jmap -dump:format=b,file=heap.hprof 12568

警告:jmap dump全堆会STW,大堆会卡顿很久,线上谨慎操作!

拿到hprof之后怎么分析?

生产服务器不适合分析大hprof,下载到本地,使用MAT(Eclipse Memory Analyzer Tool),这是行业标准工具。 MAT核心分析方向:

  1. Histogram直方图:看哪个类实例数量最多、占用内存最大
  2. Dominator Tree支配树:看谁持有这些对象,找出GC引用链,定位泄漏源头
  3. Leak Suspects自动报告:MAT会给出可疑内存泄漏候选

常见内存泄漏原因:

  1. 静态集合(static List/Map)不断add元素,永远不清空
  2. 线程池线程持有对象,线程不销毁,对象无法释放
  3. 监听器、回调没有移除,持续持有引用
  4. 数据库连接、IO流忘记关闭
  5. ThreadLocal没有remove,线程复用导致对象堆积

Arthas查看内存实时情况

# 查看堆内存使用情况,等价jstat memory # 查看对象统计 heapdump --live /tmp/heap.hprof

五、jstat:持续监控GC状态(判断GC是否异常)

很多时候不是直接OOM,而是GC越来越频繁、FullGC变多,接口慢慢变慢。 jstat用来持续观察GC指标。

# 持续监控gc,每1000ms打印一次,一共打印20次 jstat -gc 12568 1000 20

输出字段含义(重点):

  • S0C、S1C:两个Survivor容量
  • S0U、S1U:Survivor使用大小
  • EC、EU:Eden容量、使用
  • OC、OU:老年代容量、使用
  • MC、MU:元空间
  • YGC、YGCT:Minor GC次数、Minor GC总耗时
  • FGC、FGCT:Full GC次数、Full GC总耗时
  • GCT:GC总耗时

重点观察:

  1. OU(老年代使用)持续稳步上涨,YGC越来越频繁 → 大概率内存泄漏
  2. FGC持续上涨,每次FullGC后内存几乎不下降 → 泄漏实锤
  3. YGCT/FGCT单次时间很长 → STW停顿严重,需要调优收集器、堆大小

六、Arthas核心命令汇总(实战高频)

Arthas是阿里开源Java诊断工具,功能比原生命令强大很多,适合快速定位。

# 1. 查看线程(CPU高) thread -n 5 # 2. 找阻塞锁线程 thread -b # 3. 查看JVM内存状态 memory # 4. 监控方法执行耗时(定位慢接口) watch com.xxx.service.UserService queryUser '{params,returnObj,throwExp}' # 5. 查看方法调用栈 stack com.xxx.service.UserService queryUser # 6. 统计方法QPS、耗时 trace com.xxx.service.UserService queryUser # 7. 导出堆快照 heapdump --live /tmp/dump.hprof # 8. 查看类加载 sc # 9. 退出arthas stop

watch/trace/stack:适合排查接口慢,定位到底是哪一行代码、哪个第三方调用耗时久。

七、三大故障标准化排查流程图

7.1 CPU高排查流程

top找到JavaPID → top -H -p PID找到高CPU线程tid → 转16进制 → jstack搜索线程栈 → 判断死循环/GC暴涨/正则问题

7.2 死锁/线程卡死流程

接口超时堆积 → jstack导出线程栈 → 搜索Found one Java-level deadlock → 拿到锁信息和代码行 → 修改锁顺序

7.3 OOM/内存持续上涨流程

jstat观察老年代是否持续上涨 → 自动dump或者jmap导出hprof → MAT分析支配树、直方图 → 定位引用链和泄漏点

八、本篇背诵清单

  1. 线上CPU100%,你的排查步骤是什么?
  2. 怎么定位死锁?死锁四个条件?
  3. OOM和内存泄漏区别?如何自动dump堆快照?
  4. jstack、jmap、jstat各自作用?
  5. 拿到hprof堆文件,用什么工具分析,重点看什么?
  6. Arthas的thread/watch/trace/stack分别是干嘛的?
  7. 频繁FullGC,但是没有OOM,怎么排查?
  8. ThreadLocal内存泄漏原因?

九、排坑总结

  1. 不要频繁jstack、jmap大堆,STW会加重线上卡顿;优先jstat观察趋势
  2. dump文件一定要加--live,只dump存活对象,文件更小,分析更快
  3. 区分GC线程CPU高 和业务代码CPU高,二者解决方案完全不同
  4. 死锁不会自动恢复,一旦发现死锁,只能重启服务+修复代码
  5. Arthas很多公司生产环境管控严格,优先掌握原生jps/jstack/jstat/jmap

小贴士

到这里整个JVM专栏四篇正文就全部结束了:运行时数据区 → 堆分代模型 → GC算法&收集器 → 线上故障排查实战。 后面你可以基于这套理论,继续拓展JVM调优实战案例,比如G1调优参数、GC日志解读。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询