CPU 飙高怎么排查?

5 阅读6分钟

一、先明确 CPU 飙高可能意味着什么

CPU 飙高通常说明系统在“疯狂算”,常见原因有:

  • 死循环
  • 频繁 Full GC / Young GC
  • 线程空转、自旋
  • 大量字符串处理、JSON 序列化
  • 频繁排序、聚合、复杂计算
  • 大量请求打进来,业务线程被打满
  • SQL 慢导致线程堆积后伴随大量上下文切换
  • 锁竞争严重
  • JIT 编译、类加载异常
  • 某些中间件线程异常

所以不要一上来就猜代码有问题,先看现象。


二、整体排查思路

  1. 看系统整体负载
  2. 找出哪个进程占用 CPU 高
  3. 找出哪个线程占用 CPU 高
  4. 把线程 ID 转成 Java 线程栈定位代码
  5. 结合 GC、日志、监控判断根因

三、第一步:看机器整体情况

先登录服务器,查看机器负载。

1. 看 CPU 使用率

top

重点看:

  • us:用户态 CPU
  • sy:内核态 CPU
  • id:空闲 CPU
  • wa: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 高

如果线程状态很多都在:

  • RUNNABLE
  • Unsafe.park
  • ReentrantLock
  • 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 -cps 找出高 CPU 进程。

第三步

top -H -p pid 找出高 CPU 线程。

第四步

把线程 ID 转十六进制,用 jstack 查看线程栈。

第五步

结合 jstat、GC 日志、应用日志、监控平台看是不是 GC、死循环、锁竞争或热点计算导致。

第六步

如果现场不好判断,我会连续抓几次线程栈,观察是不是同一个线程一直卡在同一段代码。

这一步很关键,因为单次线程栈有时不一定准,多抓几次更容易发现问题


十一、面试里可以补充的几个高频根因

面试官一般喜欢你再说一下“常见根因”,你可以补这几个:

  • 代码里有死循环或空轮询
  • 大量对象创建导致频繁 GC
  • 线程池参数不合理,任务堆积后频繁调度
  • 大量字符串拼接、JSON 转换
  • SQL 查出来数据太大,Java 侧处理过重
  • 日志打印过多
  • 锁竞争激烈,自旋消耗 CPU
  • 第三方组件 Bug
  • 流量突增,没有限流

十二、面试回答版本

我一般按“机器、进程、线程、代码”四层来排查。
先用 top 看整机 CPU、负载和用户态系统态占比,再用 top -cps 定位高 CPU 进程。
如果是 Java 进程,我会用 top -H -p pid 找到具体高 CPU 线程,再把线程 ID 转成十六进制,结合 jstack 查看线程栈,定位线程正在执行的代码。
同时我会结合 jstat 和 GC 日志判断是否是频繁 GC 导致的 CPU 高,再结合应用日志和监控排查是不是死循环、热点计算、锁竞争、异常重试或者日志打太多。
如果一次定位不准,我会连续抓几次线程栈,看看是不是同一个线程反复卡在同一段代码,这样通常就能比较准确地找到根因。


十三、更有实战感的补充

真正线上排查时,CPU 高只是现象,根因往往是代码问题、流量问题、GC 问题或者锁竞争问题
所以不要只盯着 CPU 数字本身,而要把线程栈、GC、日志、监控结合起来一起看。