基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

如何对CPU飙高的应用进行排查

知识点图片

排查Java应用CPU飙高:核心是top+jstack定位代码,再用Arthas深入分析。

对CPU飙高的Java应用进行排查是一个非常经典且重要的技能。下面我将为你提供一个系统化、从易到难、从宏观到微观的排查思路和具体步骤。

核心排查思路

排查CPU飙高的核心思路是:

  1. 定位进程:找到是哪个Java进程在消耗CPU。
  2. 定位线程:在该进程中,找到是哪个或哪些线程在消耗CPU。
  3. 定位代码:查看这些线程正在执行什么代码。
  4. 分析原因:根据代码和线程状态,分析是业务逻辑问题、GC问题还是其他原因,并着手解决。

第一阶段:快速定位问题线程和代码(命令行“三板斧”)

这是最常用、最高效的排查方式,尤其适用于线上紧急情况。假设你的应用运行在Linux服务器上。

步骤 1:找到最耗CPU的Java进程ID (PID)

使用 top 命令,然后按 Shift + P 以CPU使用率排序。

bash
top

找到 COMMAND 列为 java%CPU 最高的那个进程,记下它的 PID
假设我们找到的 PID12345

步骤 2:找到该进程中最耗CPU的线程ID (TID)

使用 top 的线程模式,可以查看到具体线程的CPU消耗。

bash
# -H: 显示线程
# -p: 指定进程ID
top -H -p 12345

同样,按 Shift + P 排序,找到最耗CPU的那个线程,记下它的 PID (这里显示的PID实际上是线程ID,即TID)。
假设我们找到的 TID12346

步骤 3:将线程ID转换为十六进制

Java的线程堆栈 jstack 中,线程ID是以十六进制格式显示的(nid),所以我们需要转换一下。

bash
# 使用printf命令进行转换
printf "%x\n" 12346

假设转换后的结果是 303a

步骤 4:打印线程堆栈,并找到问题代码

使用JDK自带的 jstack 工具来打印指定进程的线程堆栈信息,并利用 grep 快速定位到问题线程。

bash
# jstack <PID> > dump.txt 将堆栈信息输出到文件
jstack 12345 > dump.txt

# 在文件中搜索十六进制的线程ID
# -A 30: 显示匹配行及其后的30行,以获得完整的堆栈信息
grep -A 30 303a dump.txt

现在,你会看到类似下面的输出:

plaintext
"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活动:

bash
# -gcutil: 显示GC相关区域的使用率和GC次数/时间
# <PID>: 你的Java进程ID
# 1000: 每1000毫秒(1秒)输出一次
# 10: 总共输出10次
jstat -gcutil 12345 1000 10

输出示例:

plaintext
  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

bash
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

然后选择你要诊断的Java进程。

常用命令

  • thread: 查看所有线程信息,并按CPU使用率排序。

    plaintext
    thread -n 3

    这条命令会直接列出CPU使用率最高的3个线程及其堆栈,一步到位,比 top + jstack 更方便。

  • profiler: 对应用进行CPU火焰图分析,非常直观。

    plaintext
    # 开始采样,持续30秒
    profiler start --duration 30
    
    # 获取采样结果,默认生成html格式的火焰图
    profiler getSamples
    
    # 停止(如果还没到30秒)
    profiler stop

    生成的火焰图可以清晰地看到哪个方法的调用路径消耗了最多的CPU时间(火焰图越宽,消耗CPU越多)。

  • trace: 跟踪某个方法的执行耗时。

    plaintext
    trace 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分析等,是终极性能分析利器。

常见CPU飙高原因总结

通过上述工具定位到代码后,通常会发现以下几类问题:

  1. 无限循环/死循环

    • while(true) 循环没有正确的退出条件。
    • 集合遍历的逻辑错误,导致循环无法终止。
  2. 复杂的计算或算法

    • 递归没有正确的终止条件,或递归深度过大。
    • 处理超大数据集合,进行排序、分组等高复杂度操作。
    • 正则表达式写的不好,导致“灾难性回溯”。
  3. 频繁的Full GC

    • 内存泄漏:对象被持续引用,无法被回收,导致老年代被占满。
    • 堆内存不足:分配给JVM的堆空间太小,无法容纳正常运行所需的对象。
    • 代码问题:一次性加载过多数据到内存中,或创建了生命周期很长的大对象。
  4. 高并发下的线程问题

    • 线程数过多:大量线程在就绪状态(RUNNABLE)等待CPU调度,导致上下文切换开销巨大。
    • 锁竞争激烈:虽然 BLOCKED 状态不耗CPU,但如果线程通过自旋等待锁(e.g., CAS),会消耗大量CPU。
  5. 不当的API使用

    • 在循环中反复创建和销毁昂贵的对象(如数据库连接、线程等)。
    • 使用了性能低下的库或方法。

总结排查流程

  1. 线上告警:收到CPU使用率过高告警。
  2. 快速定位
    • top 找到进程PID。
    • top -H -p <PID> 找到线程TID。
    • printf "%x\n" <TID> 转换为十六进制。
    • jstack <PID> 抓取堆栈,搜索十六进制TID,定位到具体代码行。
  3. 初步分析
    • 如果是业务代码死循环或复杂计算,直接修改代码。
    • 如果堆栈在GC相关线程,或者业务线程频繁变化且GC日志异常,转向GC分析。
  4. 深入分析
    • 使用 jstat -gcutil 确认GC是否频繁。如果是,通过 jmap dump内存快照,使用MAT等工具分析内存泄漏。
    • 使用 Arthasthread -n 3profiler 进行动态分析,生成火焰图,找到热点代码。
    • 使用 JMC/JFR 进行全面的性能记录和分析,获取最详尽的报告。
  5. 解决问题:根据分析结果,修复代码、调整JVM参数(如堆大小、GC收集器)或优化系统架构。

这套组合拳几乎可以解决绝大多数Java应用CPU飙高的问题。建议从第一阶段的“三板斧”开始,如果问题无法解决,再逐步使用更高级的工具。

00:00
00:00