CPU突然飙升到99%,作为开发者,你应该怎么排查呢?

63 阅读8分钟

CPU突然飙升到99%,作为开发者,你应该怎么排查呢?

想象一下,你是一位每天勤勤恳恳写代码的程序员,有一天你发现CPU居然被打到了99%,那你应该怎么办呢?是草草重启了事吗?no no no,作为一名合格的程序员,你必须要进行系统化的排查,找到根本原因,这不仅仅能解决燃眉之急,还能学到相关经验,等到下一次遇到相同的问题,你便不会手忙脚乱。

那今天星星将会带你深入理解,遇到这个问题的时候,你应该如何系统的进行排查,并掌握防患于未然的相关经验(毕竟预防远比补救更重要)。构建一个涵盖事前预防、事中定位、事后根治的完整治理体系。

1.既然已经发生了,那先想办法解燃眉之急。

  • 定位是否是Java进程或线程捣的鬼
  • 第一种,使用系统工具和JDK自带的jstack工具
  • 第二种,使用Arthas探测工具

2.探寻为啥好端端的会导致CPU飙升99%。

  • 无尽循环
  • GC层面,内存管理的失控
  • 锁竞争

3.预防总比事情发生了再解决要好。

1.既然已经发生了,那先想办法解燃眉之急。

定位是否是Java进程或线程捣的鬼

  1. 全局视野,锁定目标 使用 top 命令,查看整个服务器的资源状况。在进程列表中,找到CPU使用率最高的那个,并确认其是否为我们的Java应用进程。记下它的 PID(进程ID)

  2. 深入敌后,揪出问题线程 一个Java进程包含众多线程,我们需要找到那个最消耗CPU的“害群之马”。

    bash

    top -H -p [你的Java进程PID]
    

    这个命令会列出该进程内所有线程的CPU使用情况。找到那个持续占用CPU最高的线程,记下它的 线程PID(十进制)

第一种,使用系统工具和JDK自带的jstack工具

  1. 转换线程ID:将上一步找到的高CPU线程PID(十进制)转换为十六进制。

    bash

    printf "%x\n" [十进制线程PID]
    

    得到其 nid

  2. 捕获线程快照:使用 jstack 抓取当前Java进程的所有线程堆栈信息。

    bash

    jstack [Java进程PID] > jstack.log
    
  3. 线索比对:在 jstack.log 文件中,搜索刚才转换得到的十六进制 nid。你会定位到具体的Java线程、它的状态(通常是 RUNNABLE)以及完整的调用栈。这个调用栈直接告诉你,该线程正在执行什么代码,像什么线程名称呀,线程状态呀,哪个方法哪行代码消耗了最多的CPU呀,你都可以很清楚的看到。

第二种,使用Arthas探测工具

如果环境允许,Arthas能让你进行交互式、动态的诊断,如同给应用做了一次“实时CT”。这里因为可能有一部分朋友没用过,所以讲一下它的安装

下载jar包—>启动Arthas服务—>使用Arthas找到占用CPU最高的进程—>找到占用CPU最高线程—>查看堆栈信息

**1.下载和启动:**这是最简单、最快捷的方式,尤其适合个人开发和学习。它会自动下载最新的稳定版本。

命令如下:

bash

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

或者使用 wget

bash

wget https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

执行后的步骤:

  1. 命令运行后,Arthas 会检测到当前机器上所有的 Java 进程,并列出列表。
  2. 你需要输入列表中最前面 进程号对应的数字(比如 1),然后按回车。
  3. Arthas 会附加到目标进程上,并完成核心的加载。看到 [arthas@进程号]$ 提示符就表示成功了,可以开始输入各种诊断命令。

2. 快速定位CPU占用最高的进程和线程

bash

# 启动Arthas,选择目标Java进程
java -jar arthas-boot.jar

# 或者直接附加到指定PID
java -jar arthas-boot.jar [pid]

# 查看实时线程CPU占用情况
thread -n 3

# 持续监控线程状态
thread -i 1000

# 查看所有线程的CPU时间
thread

3. 分析热点方法

bash

# 采样CPU使用情况,持续5秒
profiler start --duration 5

# 查看采样结果
profiler stop

# 或者使用dashboard命令实时观察
dashboard

4. 深入分析具体线程

当找到CPU占用高的线程ID后:

bash

# 查看特定线程的堆栈信息
thread [thread-id]

# 查看线程状态和运行时间
thread -b

5. 方法执行监控

bash

# 监控特定方法的执行时间和调用次数
watch com.example.YourClass yourMethod "{params, returnObj, throwExp}" -n 5 -x 3

# 跟踪方法调用路径
trace com.example.YourClass yourMethod

6.内存和GC分析

bash

# 查看堆内存情况
dashboard

# 监控GC活动
vmtool --action getInstances --className java.lang.management.GarbageCollectorMXBean --express 'instances.length'

2.探寻为啥好端端的会导致CPU飙升99%。

通过上述手段,我们通常能将问题归为以下三类。

疯狂递归、循环

  • 特征jstack 日志显示某线程长期处于 RUNNABLE 状态,且调用栈始终停留在某个循环体或递归方法中。

  • 典型案例

    • 死循环:循环条件永远为真,或边界条件处理错误导致无法退出。

    java

    // 危险的边界案例
    while (i <= list.size()) { // 当i等于size()时,会引发越界,或逻辑错误导致无法退出
        // ...
    }
    
    • 无限递归:缺少基准情形,或递归深度失控。

    java

    // 错误的递归:缺少基准情形
    public void recursiveMethod() {
        recursiveMethod(); // 直接调用自身,无限递归直至栈溢出
    }
    

GC——内存管理的失控

  • 特征:使用 jstat -gcutil [PID] 1s 观察,发现 FGC(Full GC)次数 急剧增加,FGCT(Full GC Time) 耗时很长,且GC线程消耗大量CPU。
  • 根本原因
    1. 内存泄漏:对象被意外地(如通过静态集合)持有引用而无法被GC回收,最终撑爆老年代,触发频繁Full GC。
    2. 短命大对象:频繁创建大数组或大对象(如从数据库一次加载过多数据),直接进入老年代,加速Full GC的发生。
    3. JVM参数不合理:堆内存设置过小,Survivor区比例不当,导致对象过早晋升老年代。

锁竞争

  • 特征:大量线程处于 BLOCKED 状态,jstack 可能直接报告发现死锁。从宏观上看,线程因争抢资源而陷入等待,CPU可能因线程调度和少量成功获取锁的线程而显得繁忙。

  • 典型场景

    java

    // 粗粒度的同步方法,成为系统瓶颈
    public synchronized void doHeavyWork() {
        // 长时间的IO操作或复杂计算
    }
    

    当多个线程调用此方法时,其余线程全部阻塞,系统吞吐量骤降。

3.预防总比事情发生了再解决要好。

治疗的最高境界是“治未病”。通过以下措施,我们可以将CPU峰值问题消灭在萌芽状态。

1. 编码规范的“军规”

  • 循环与递归:代码审查中,必须严格检查循环终止条件和递归的基准情形。对递归深度设置安全阈值。
  • 资源管理
    • 使用 try-with-resources 确保连接、文件流等资源被绝对释放。
    • 对于缓存等静态集合,必须设置大小上限和过期策略,并提供清理入口。
  • 并发编程
    • 禁用无界队列线程池,一律使用 ThreadPoolExecutor 构造函数,明确指定队列容量和拒绝策略。
    • 减小锁粒度,优先使用并发容器(如 ConcurrentHashMap),考虑使用读写锁(ReadWriteLock)替代排他锁。

2. 性能与压测的“常态化”

  • 上线前压测:任何新功能或重大变更上线前,必须进行压力测试,找到系统的性能拐点和瓶颈。
  • Profiling工具的使用:在压测或预发环境,定期使用 Async-Profiler 等工具生成 火焰图,直观地发现代码中的性能热点,并进行针对性优化。

3. 监控与告警的“天网”

  • 建立多层次监控
    • 系统层:CPU、内存、磁盘IO。
    • JVM层:堆内存使用率、GC频率与耗时、线程池状态。
    • 应用层:QPS、RT(响应时间)、错误率。
  • 设置智能告警:不仅监控CPU使用率,更要关联GC频率、线程数等指标。例如“CPU持续 > 90% FGC次数1分钟内超过5次”的告警,比单一指标更精准。

4. JVM参数的“调优”

  • 根据压测结果和机器配置,合理设置堆大小(-Xms, -Xmx),新生代与老年代的比例。
  • 选择合适的GC器(如G1),并设置合理的预期停顿时间(-XX:MaxGCPauseMillis)。
  • 强制开启GC日志,这是事后排查GC问题的唯一可靠依据。

总结一下,其实你发现这个问题你只需要知道解决的顺序了,那它就不棘手了,所以大多情况下,我们只需要记下顺序,那就没啥大问题了。这里我还是推荐使用Arthas,它功能非常多之外,对比着jstack来看,在线上场景下,使用jstack有时会碰到问题,如果这个线程已经忙的一点转圈的余地也没有了,jstack命令可能就会执行失败。

好的,今天的内容到这里就结束了,如果这篇文章有帮到你的话,我会很开心,你也可以点一下赞支持一下,我们明天再见。