如何对CPU飙高的应用进行排查
排查Java应用CPU飙高:核心是
top+jstack定位代码,再用Arthas深入分析。
对CPU飙高的Java应用进行排查是一个非常经典且重要的技能。下面我将为你提供一个系统化、从易到难、从宏观到微观的排查思路和具体步骤。
核心排查思路
排查CPU飙高的核心思路是:
- 定位进程:找到是哪个Java进程在消耗CPU。
- 定位线程:在该进程中,找到是哪个或哪些线程在消耗CPU。
- 定位代码:查看这些线程正在执行什么代码。
- 分析原因:根据代码和线程状态,分析是业务逻辑问题、GC问题还是其他原因,并着手解决。
第一阶段:快速定位问题线程和代码(命令行“三板斧”)
这是最常用、最高效的排查方式,尤其适用于线上紧急情况。假设你的应用运行在Linux服务器上。
步骤 1:找到最耗CPU的Java进程ID (PID)
使用 top 命令,然后按 Shift + P 以CPU使用率排序。
top
找到 COMMAND 列为 java 且 %CPU 最高的那个进程,记下它的 PID。
假设我们找到的 PID 是 12345。
步骤 2:找到该进程中最耗CPU的线程ID (TID)
使用 top 的线程模式,可以查看到具体线程的CPU消耗。
# -H: 显示线程
# -p: 指定进程ID
top -H -p 12345
同样,按 Shift + P 排序,找到最耗CPU的那个线程,记下它的 PID (这里显示的PID实际上是线程ID,即TID)。
假设我们找到的 TID 是 12346。
步骤 3:将线程ID转换为十六进制
Java的线程堆栈 jstack 中,线程ID是以十六进制格式显示的(nid),所以我们需要转换一下。
# 使用printf命令进行转换
printf "%x\n" 12346
假设转换后的结果是 303a。
步骤 4:打印线程堆栈,并找到问题代码
使用JDK自带的 jstack 工具来打印指定进程的线程堆栈信息,并利用 grep 快速定位到问题线程。
# jstack <PID> > dump.txt 将堆栈信息输出到文件
jstack 12345 > dump.txt
# 在文件中搜索十六进制的线程ID
# -A 30: 显示匹配行及其后的30行,以获得完整的堆栈信息
grep -A 30 303a dump.txt
现在,你会看到类似下面的输出:
"pool-1-thread-1" #10 prio=5 os_prio=0 tid=0x00007f... nid=0x303a runnable [0x00007f...]
java.lang.Thread.State: RUNNABLE
at com.example.MyController.heavyCalculation(MyController.java:88)
at com.example.MyController.processRequest(MyController.java:45)
at sun.reflect.GeneratedMethodAccessor101.invoke(Unknown Source)
... (省略)
分析:
nid=0x303a:这就是我们找到的那个耗CPU的线程。java.lang.Thread.State: RUNNABLE:线程处于可运行状态,说明它确实在消耗CPU(而不是在等待I/O或锁)。- 堆栈信息:从上往下看,最上面的
com.example.MyController.heavyCalculation(MyController.java:88)就是当前线程正在执行的方法。
到这里,你已经成功定位到了消耗CPU的具体代码行。接下来就是分析这行代码为什么会消耗大量CPU。
第二阶段:深入分析与常用工具
有时,问题不是一个简单的死循环,可能更复杂,比如GC问题、JIT编译问题或需要动态分析。
1. 分析GC是否是元凶
当CPU飙高时,Full GC频繁是一个非常常见的原因。JVM会花费大量时间在垃圾回收上,导致业务线程暂停,CPU使用率上升。
使用 jstat 命令来监控GC活动:
# -gcutil: 显示GC相关区域的使用率和GC次数/时间
# <PID>: 你的Java进程ID
# 1000: 每1000毫秒(1秒)输出一次
# 10: 总共输出10次
jstat -gcutil 12345 1000 10
输出示例:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 99.50 25.10 98.80 97.20 95.50 100 0.500 99 50.123 50.623
关注点:
FGC(Full GC次数) 和FGCT(Full GC总耗时):如果这两个值在短时间内快速、持续增长,那么CPU飙高很可能就是由频繁的Full GC引起的。O(Old区使用率):如果Old区使用率持续在90%以上,居高不下,很容易触发Full GC。
原因:通常是内存泄漏、堆内存设置过小或创建了大量短时间内无法回收的大对象。
2. 使用动态诊断工具 (Arthas)
对于更复杂或间歇性的问题,jstack 抓取的瞬间快照可能不足以说明问题。此时,推荐使用阿里巴巴开源的 Arthas,它可以在不重启服务的情况下,动态地诊断应用。
安装和启动Arthas:
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
然后选择你要诊断的Java进程。
常用命令:
thread: 查看所有线程信息,并按CPU使用率排序。plaintextthread -n 3这条命令会直接列出CPU使用率最高的3个线程及其堆栈,一步到位,比
top+jstack更方便。profiler: 对应用进行CPU火焰图分析,非常直观。plaintext# 开始采样,持续30秒 profiler start --duration 30 # 获取采样结果,默认生成html格式的火焰图 profiler getSamples # 停止(如果还没到30秒) profiler stop生成的火焰图可以清晰地看到哪个方法的调用路径消耗了最多的CPU时间(火焰图越宽,消耗CPU越多)。
trace: 跟踪某个方法的执行耗时。plaintexttrace com.example.MyController heavyCalculation可以帮你分析方法内部的调用耗时,找到性能瓶颈。
3. 使用图形化界面工具
如果你能连接到服务器的图形化界面(或通过JMX远程连接),可以使用以下工具:
JVisualVM: JDK自带的工具,位于
JAVA_HOME/bin/jvisualvm。- CPU采样器:可以实时查看方法级的CPU消耗。
- 线程视图:可以实时查看各线程的状态和CPU时间。
- 监视:可以看到CPU、内存、GC的实时曲线图。
Java Mission Control (JMC): 更强大的分析工具,与 Java Flight Recorder (JFR) 结合使用。
- 启动JFR进行一段事件的记录:
jcmd <PID> JFR.start duration=60s filename=myrecording.jfr - 记录结束后,用JMC打开
myrecording.jfr文件。 - JMC提供了极其详细的报告,包括代码热点、锁竞争、GC分析等,是终极性能分析利器。
- 启动JFR进行一段事件的记录:
常见CPU飙高原因总结
通过上述工具定位到代码后,通常会发现以下几类问题:
无限循环/死循环:
while(true)循环没有正确的退出条件。- 集合遍历的逻辑错误,导致循环无法终止。
复杂的计算或算法:
- 递归没有正确的终止条件,或递归深度过大。
- 处理超大数据集合,进行排序、分组等高复杂度操作。
- 正则表达式写的不好,导致“灾难性回溯”。
频繁的Full GC:
- 内存泄漏:对象被持续引用,无法被回收,导致老年代被占满。
- 堆内存不足:分配给JVM的堆空间太小,无法容纳正常运行所需的对象。
- 代码问题:一次性加载过多数据到内存中,或创建了生命周期很长的大对象。
高并发下的线程问题:
- 线程数过多:大量线程在就绪状态(RUNNABLE)等待CPU调度,导致上下文切换开销巨大。
- 锁竞争激烈:虽然
BLOCKED状态不耗CPU,但如果线程通过自旋等待锁(e.g.,CAS),会消耗大量CPU。
不当的API使用:
- 在循环中反复创建和销毁昂贵的对象(如数据库连接、线程等)。
- 使用了性能低下的库或方法。
总结排查流程
- 线上告警:收到CPU使用率过高告警。
- 快速定位:
top找到进程PID。top -H -p <PID>找到线程TID。printf "%x\n" <TID>转换为十六进制。jstack <PID>抓取堆栈,搜索十六进制TID,定位到具体代码行。
- 初步分析:
- 如果是业务代码死循环或复杂计算,直接修改代码。
- 如果堆栈在GC相关线程,或者业务线程频繁变化且GC日志异常,转向GC分析。
- 深入分析:
- 使用
jstat -gcutil确认GC是否频繁。如果是,通过jmapdump内存快照,使用MAT等工具分析内存泄漏。 - 使用 Arthas 的
thread -n 3或profiler进行动态分析,生成火焰图,找到热点代码。 - 使用 JMC/JFR 进行全面的性能记录和分析,获取最详尽的报告。
- 使用
- 解决问题:根据分析结果,修复代码、调整JVM参数(如堆大小、GC收集器)或优化系统架构。
这套组合拳几乎可以解决绝大多数Java应用CPU飙高的问题。建议从第一阶段的“三板斧”开始,如果问题无法解决,再逐步使用更高级的工具。