CPU突然飙升到99%,作为开发者,你应该怎么排查呢?
想象一下,你是一位每天勤勤恳恳写代码的程序员,有一天你发现CPU居然被打到了99%,那你应该怎么办呢?是草草重启了事吗?no no no,作为一名合格的程序员,你必须要进行系统化的排查,找到根本原因,这不仅仅能解决燃眉之急,还能学到相关经验,等到下一次遇到相同的问题,你便不会手忙脚乱。
那今天星星将会带你深入理解,遇到这个问题的时候,你应该如何系统的进行排查,并掌握防患于未然的相关经验(毕竟预防远比补救更重要)。构建一个涵盖事前预防、事中定位、事后根治的完整治理体系。
1.既然已经发生了,那先想办法解燃眉之急。
- 定位是否是Java进程或线程捣的鬼
- 第一种,使用系统工具和JDK自带的jstack工具
- 第二种,使用Arthas探测工具
2.探寻为啥好端端的会导致CPU飙升99%。
- 无尽循环
- GC层面,内存管理的失控
- 锁竞争
3.预防总比事情发生了再解决要好。
1.既然已经发生了,那先想办法解燃眉之急。
定位是否是Java进程或线程捣的鬼
-
全局视野,锁定目标 使用
top命令,查看整个服务器的资源状况。在进程列表中,找到CPU使用率最高的那个,并确认其是否为我们的Java应用进程。记下它的 PID(进程ID)。 -
深入敌后,揪出问题线程 一个Java进程包含众多线程,我们需要找到那个最消耗CPU的“害群之马”。
bash
top -H -p [你的Java进程PID]这个命令会列出该进程内所有线程的CPU使用情况。找到那个持续占用CPU最高的线程,记下它的 线程PID(十进制)。
第一种,使用系统工具和JDK自带的jstack工具
-
转换线程ID:将上一步找到的高CPU线程PID(十进制)转换为十六进制。
bash
printf "%x\n" [十进制线程PID]得到其 nid。
-
捕获线程快照:使用
jstack抓取当前Java进程的所有线程堆栈信息。bash
jstack [Java进程PID] > jstack.log -
线索比对:在
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
执行后的步骤:
- 命令运行后,Arthas 会检测到当前机器上所有的 Java 进程,并列出列表。
- 你需要输入列表中最前面 进程号对应的数字(比如
1),然后按回车。 - 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。 - 根本原因:
- 内存泄漏:对象被意外地(如通过静态集合)持有引用而无法被GC回收,最终撑爆老年代,触发频繁Full GC。
- 短命大对象:频繁创建大数组或大对象(如从数据库一次加载过多数据),直接进入老年代,加速Full GC的发生。
- 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命令可能就会执行失败。
好的,今天的内容到这里就结束了,如果这篇文章有帮到你的话,我会很开心,你也可以点一下赞支持一下,我们明天再见。