凌晨,生产环境的核心服务CPU使用率持续超过95%,接口超时,错误日志飙升。面对一台“高烧不退”的服务器,盲目重启只能暂时缓解,唯有精准定位病灶才能根治问题。本文将演练一场从宏观监控到微观代码的“全链路CPU飙高侦破案”,助你不仅能快速救火,更能洞悉火因。
一、 CPU 100%的常见诱因分析
CPU的疯狂工作,本质上是线程在疯狂执行。主要诱因可分为以下几类:
- 计算密集型任务:最直接的猜想。例如没有退出条件的死循环(
while(true)),或存在逻辑错误的复杂递归调用。
- “伪”计算任务——忙等待:线程并未进行有效计算,但因条件不满足而空转,持续占用CPU时间片。例如
while(!condition) 的空循环。
- 频繁的垃圾回收(GC):当Java堆内存不足或存在内存泄漏时,垃圾回收器(特别是Full GC)会频繁启动以释放空间。GC线程是高优先级线程,其频繁工作会直接吞噬大量CPU资源。
- 激烈的锁竞争:大量线程竞争同一把锁(如
synchronized 或 ReentrantLock),导致线程在 BLOCKED 状态和 RUNNABLE 状态间快速切换,宏观上表现为CPU饱和。更隐蔽的是活锁,线程间不断响应但都无法推进。
曾有一个案例:订单服务CPU飙高,初步怀疑计算逻辑问题,最终定位竟是数据库连接池配置过小。大量业务线程在短时间内不断尝试获取连接、快速失败、立即重试,形成了高频率的“忙等待”,导致CPU使用率居高不下。
二、 实战排查三部曲:从宏观到微观
下图描绘了完整的排查路径,可作为本次“侦破行动”的作战地图:
辅助诊断工具
vmstat
jstat -gcutil
Arthas
监控告警: CPU 100%
↓
第一步: 系统级定位(top + ps)
确定异常进程PID
↓
第二步: 线程级分析(top -Hp / jstack)
确定高CPU线程ID并转换HEX格式
↓
第三步: 代码级溯源(分析jstack堆栈)
定位问题代码行
↓
原因分析与修复
死循环/无限递归
锁竞争/活锁
频繁GC
第一步:系统级定位——找到异常进程
首先,需要在服务器层面定位是哪个进程导致CPU使用率过高。
- 使用
top 命令:登录服务器,输入 top,按 Shift + P 依据CPU使用率排序。记录排在首位进程的 PID。
- 使用
ps 命令辅助确认:
ps aux --sort=-%cpu | head -10
此命令能清晰列出消耗CPU最高的前10个进程及其命令行信息,方便判断是否为你的Java应用。
第二步:线程级分析——揪出高消耗线程
定位到进程后,需深入其内部,找出消耗CPU的具体线程。
- 查看进程内线程CPU消耗:
top -Hp <你的PID>
同样按 Shift + P 排序,记下CPU使用率异常高的线程的 PID(此处为线程ID,记为 TID)。
- 转换线程ID格式:
jstack 输出中的线程ID为十六进制,需进行转换。
printf "%x\n" <你的TID>
记录转换后的十六进制值,例如 0x4a3d。
- 获取Java线程堆栈:使用
jstack 命令生成线程快照。
jstack -l <你的进程PID> > ./jstack_dump_$(date +%Y%m%d%H%M%S).log
第三步:代码级溯源——锁定问题代码行
这是最关键的一步,将高CPU线程与具体代码关联起来。
- 在堆栈文件中搜索:用编辑器打开上一步生成的
.log 文件,搜索转换得到的十六进制线程ID(如 4a3d,不需要 0x 前缀)。
- 分析线程堆栈:找到该线程的堆栈信息,其中包含了正在执行的类、方法及行号。
"Thread-0" #16 prio=5 os_prio=0 tid=0x00007f4b8810a800 nid=0x4a3d runnable [0x00007f4b7effe000]
java.lang.Thread.State: RUNNABLE
at com.example.demo.DeadLoopService.run(DeadLoopService.java:10) // 明确指出了问题文件和行号
at java.lang.Thread.run(Thread.java:748)
如上所示,线程 0x4a3d 处于 RUNNABLE 状态,并停留在 DeadLoopService.java:10,这很可能是一个死循环。
- 理解线程状态:
jstack 中的线程状态是关键线索:
- RUNNABLE:线程正在JVM中执行或等待操作系统CPU时间片。高CPU线程常为此状态。
- BLOCKED:线程被阻塞,等待获取监视器锁(如进入
synchronized 区域)。
- WAITING / TIMED_WAITING:线程在等待某个条件(如
Object.wait()),通常不消耗CPU。
辅助诊断工具
除了核心命令,以下工具能提供多维度信息辅助判断:
vmstat:查看系统整体的上下文切换(cs)、中断次数(in)。数值异常高可能暗示锁竞争激烈或I/O问题。
jstat -gcutil <pid> 1000 10:每秒采样一次GC信息,连续10次。若FGC(Full GC次数)快速增加且OU(老年代使用率)居高不下,则CPU飙高可能由频繁Full GC引起。
- Arthas:阿里开源的Java诊断利器。在紧急情况下,可直接附加到目标进程,使用
thread -n 3 命令直接找出最忙碌的3个线程及其堆栈,极大提升排查效率。
三、 经典场景解析
场景一:无退出条件的循环(死循环)
public class DeadLoopDemo {
public static void main(String[] args) {
new Thread(() -> {
while (true) { // 循环条件永远为真
int i = 0;
i++;
}
}, "My-DeadLoop-Thread").start();
}
}
排查结果:jstack 中该线程状态为 RUNNABLE,堆栈停留在包含 while (true) 的代码行。
场景二:递归调用缺少出口
public class RecursionDemo {
public void faultyRecursion(int num) {
if (num > 10000) { // 基准条件可能因逻辑错误永远无法满足
return;
}
faultyRecursion(num + 1); // 无限递归
}
}
排查结果:线程堆栈会显示非常深的、重复的方法调用轨迹,最终可能抛出 StackOverflowError。
场景三:激烈的锁竞争
public class LockCompetitionDemo {
private static final Object LOCK = new Object();
public void highContentionMethod() {
synchronized (LOCK) { // 多个线程激烈竞争此锁
try {
Thread.sleep(1000); // 持有锁进行耗时操作,加剧竞争
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
排查结果:jstack 中会看到一个线程处于 RUNNABLE 并持有锁(显示 locked <0x...>),同时大量其他线程处于 BLOCKED 状态,都在等待同一个锁(显示 waiting to lock <0x...>)。
四、 排查心法与总结
- 保留现场优先:出现CPU飙高时,不要第一时间重启。应先执行
top -> top -Hp -> jstack 三部曲,保存堆栈文件以供分析。
- 遵循排查口诀:“先定进程,再定线程,堆栈之中找元凶”。用十进制PID定位进程,将十进制TID转为十六进制,最后在
jstack 输出中定位对应线程堆栈。
- 线程状态即线索:长期
RUNNABLE 且高CPU,查业务逻辑(如死循环);大量线程 BLOCKED,查锁竞争;结合 jstat 判断是否由频繁GC导致。
- 善用现代工具:在条件允许时,使用如 Arthas 的
thread -n、dashboard 等命令可以显著提升诊断效率。
- 根治优于重启:定位到代码后,应分析根本原因(逻辑缺陷、资源竞争、不合理配置等),通过修改代码或优化设计来解决问题,而非仅仅依赖重启。
|