一、先明确 CPU 飙高可能意味着什么
CPU 飙高通常说明系统在“疯狂算”,常见原因有:
- 死循环
- 频繁 Full GC / Young GC
- 线程空转、自旋
- 大量字符串处理、JSON 序列化
- 频繁排序、聚合、复杂计算
- 大量请求打进来,业务线程被打满
- SQL 慢导致线程堆积后伴随大量上下文切换
- 锁竞争严重
- JIT 编译、类加载异常
- 某些中间件线程异常
所以不要一上来就猜代码有问题,先看现象。
二、整体排查思路
- 看系统整体负载
- 找出哪个进程占用 CPU 高
- 找出哪个线程占用 CPU 高
- 把线程 ID 转成 Java 线程栈定位代码
- 结合 GC、日志、监控判断根因
三、第一步:看机器整体情况
先登录服务器,查看机器负载。
1. 看 CPU 使用率
top
重点看:
us:用户态 CPUsy:内核态 CPUid:空闲 CPUwa:IO 等待load average:系统负载
怎么判断
- 如果
us很高,通常是业务代码消耗 CPU - 如果
sy很高,可能是系统调用、网络、中断、线程切换多 - 如果
wa很高,不一定是 CPU 问题,可能是磁盘 IO 问题 - 如果 load 很高但 CPU 不一定满,可能有锁竞争或 IO 阻塞
四、第二步:定位哪个进程 CPU 高
top -c
或者:
ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%cpu | head
这样可以看到到底是:
- Java 进程高
- MySQL 高
- Nginx 高
- 还是别的进程高
如果是 Java 进程,就继续往下查线程。
五、第三步:定位哪个线程 CPU 高
假设 Java 进程 PID 是 12345。
top -H -p 12345
这个命令可以看到该 Java 进程下每个线程的 CPU 使用情况。
例如发现某个线程:
- 线程 ID:
23456 - CPU 占用很高
就记下这个线程 ID。
六、第四步:把线程 ID 转成十六进制
因为 jstack 打印出来的线程 nid 一般是十六进制,所以要转换一下。
printf "%x\n" 23456
假设输出:
5ba0
七、第五步:用 jstack 定位具体代码
jstack 12345 | grep -A 20 5ba0
或者先导出:
jstack 12345 > jstack.log
再搜索:
grep -A 30 "nid=0x5ba0" jstack.log
这样就能看到这个高 CPU 线程在执行什么代码。
八、常见几类定位结果
1. 死循环 / 空转
比如线程栈一直卡在某个 while 循环里:
while (true) {
// 没有阻塞,没有休眠
}
或者 CAS 自旋过久。
这种通常 CPU 会非常高,而且线程栈多次抓取几乎一样。
2. GC 导致 CPU 高
可以用:
jstat -gcutil 12345 1000 10
看 GC 情况。
如果发现:
- YGC 很频繁
- FGC 次数高
- GC 时间占比大
那说明 CPU 高可能不是业务线程本身,而是 GC 线程在忙。
进一步可以看 GC 日志:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xlog:gc*
常见原因:
- 对象创建过快
- 大对象太多
- 内存设置不合理
- 内存泄漏导致频繁 Full GC
3. 热点方法计算量太大
比如线程栈显示一直在:
- JSON 序列化
- 大量正则匹配
- 排序
- 递归计算
- Excel 导出
- 大集合 stream 处理
这说明是业务代码本身过于耗 CPU。
4. 锁竞争 / 自旋导致 CPU 高
如果线程状态很多都在:
RUNNABLEUnsafe.parkReentrantLock- CAS 重试
可能是锁竞争严重,或者某些无锁结构在高并发下频繁自旋。
5. 频繁异常打印
有些系统 CPU 高,不是因为业务逻辑复杂,而是因为异常一直在抛、日志一直在打。
比如一个循环里不断报错:
- 捕获异常后继续重试
- 每次都打印完整堆栈
这种会非常耗 CPU。
九、实战里我还会结合这些工具
1. pidstat
看进程和线程更细粒度 CPU 情况:
pidstat -p 12345 -t 1
2. vmstat
看整体上下文切换、系统态情况:
vmstat 1
重点关注:
r:运行队列cs:上下文切换us/sy
3. mpstat
看是不是某几个核被打满:
mpstat -P ALL 1
4. arthas
Java 排查很好用,尤其在线上。
例如:
thread -n 5
直接看最忙线程。
再结合:
dashboard
trace
watch
profiler start
profiler stop
Arthas 在线排查比单纯 jstack 更方便。
十、如果是线上 Java 服务,排查步骤如下
第一步
用 top 看整机 CPU 和负载。
第二步
用 top -c 或 ps 找出高 CPU 进程。
第三步
用 top -H -p pid 找出高 CPU 线程。
第四步
把线程 ID 转十六进制,用 jstack 查看线程栈。
第五步
结合 jstat、GC 日志、应用日志、监控平台看是不是 GC、死循环、锁竞争或热点计算导致。
第六步
如果现场不好判断,我会连续抓几次线程栈,观察是不是同一个线程一直卡在同一段代码。
这一步很关键,因为单次线程栈有时不一定准,多抓几次更容易发现问题。
十一、面试里可以补充的几个高频根因
面试官一般喜欢你再说一下“常见根因”,你可以补这几个:
- 代码里有死循环或空轮询
- 大量对象创建导致频繁 GC
- 线程池参数不合理,任务堆积后频繁调度
- 大量字符串拼接、JSON 转换
- SQL 查出来数据太大,Java 侧处理过重
- 日志打印过多
- 锁竞争激烈,自旋消耗 CPU
- 第三方组件 Bug
- 流量突增,没有限流
十二、面试回答版本
我一般按“机器、进程、线程、代码”四层来排查。
先用top看整机 CPU、负载和用户态系统态占比,再用top -c或ps定位高 CPU 进程。
如果是 Java 进程,我会用top -H -p pid找到具体高 CPU 线程,再把线程 ID 转成十六进制,结合jstack查看线程栈,定位线程正在执行的代码。
同时我会结合jstat和 GC 日志判断是否是频繁 GC 导致的 CPU 高,再结合应用日志和监控排查是不是死循环、热点计算、锁竞争、异常重试或者日志打太多。
如果一次定位不准,我会连续抓几次线程栈,看看是不是同一个线程反复卡在同一段代码,这样通常就能比较准确地找到根因。
十三、更有实战感的补充
真正线上排查时,CPU 高只是现象,根因往往是代码问题、流量问题、GC 问题或者锁竞争问题。
所以不要只盯着 CPU 数字本身,而要把线程栈、GC、日志、监控结合起来一起看。