JVMNotes

第 26 章:CPU 高排查

zjc 于 2026-01-26 发布

这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 CPU 高不是根因,而是现象。JVM 场景下,CPU 高可能来自业务计算、GC、JIT 编译、序列化、正则回溯、锁自旋,也可能是容器 CPU 配额不足导致的一点小负载就打满。

本章给出一套从系统到 Java 方法、再到根因的排查路径。

26.1 先定义问题

排查前先确认四个事实:

  1. 是 user CPU 高,还是 sys CPU 高;
  2. 是持续高,还是毛刺;
  3. 是单实例,还是所有实例;
  4. 是否伴随延迟、超时、GC 或错误率变化。
现象 方向
user 高,业务慢 业务方法、序列化、GC
sys 高 线程切换、系统调用、IO、锁
iowait 高 磁盘 IO
单实例高 实例异常、请求倾斜、节点故障
全部实例高 流量、代码、数据特征、下游
启动期高 JIT、类加载、预热
GC 线程高 内存压力

26.2 Linux 基础命令

查看进程:

top -p <pid>

查看线程:

top -H -p <pid>

查看线程使用率并输出线程号:

ps -L -p <pid> -o pid,tid,%cpu,%mem,state,comm --sort=-%cpu | head -20

将十进制线程号转十六进制:

printf "%x\n" <tid>

在线程 dump 中搜索:

nid=0x<hex-tid>

查看系统状态:

vmstat 1
pidstat -p <pid> -t 1
iostat -x 1

26.3 判断 CPU 去向

进程 CPU 高
  |
  +-- 业务线程?
  |     +-- 计算密集
  |     +-- 正则回溯
  |     +-- JSON/序列化
  |     +-- 大集合处理
  |
  +-- GC 线程?
  |     +-- 分配过快
  |     +-- 堆不足
  |     +-- Live 数据增长
  |
  +-- JIT 编译线程?
  |     +-- 启动预热
  |     +-- 热点变化
  |
  +-- 其他 JVM 线程?
        +-- 编译、压缩、类加载

如果高 CPU 线程名形如:

"GC Thread#0"
"G1 Conc#0"
"C2 CompilerThread0"

则优先分析 GC 和 JIT,而不是业务代码。

26.4 业务线程高 CPU

流程:

top -H -p <pid>
printf "%x\n" <tid>
jcmd <pid> Thread.print > thread.txt

找到线程栈后,例如:

"http-nio-8080-exec-5" RUNNABLE
   at java.util.regex.Pattern$Curly.match0(...)
   at java.util.regex.Pattern.matcher(...)
   at com.demo.SearchService.match(...)
   at com.demo.SearchController.search(...)

这说明热点在正则匹配。继续检查:

  1. 正则来源是否用户可控;
  2. 是否存在嵌套量词;
  3. 输入长度是否过大;
  4. 是否有超时保护;
  5. 是否可以换成安全匹配方式。

26.5 使用 JFR 定位热点

开启或 dump JFR 后,在 JMC 的 Code 页查看 Execution Sample。

典型热点:

com.demo.ReportService.aggregate
  com.demo.ReportMapper.select
  com.demo.ReportController.download

继续用 Arthas trace:

trace com.demo.ReportService aggregate '#cost > 100' -n 10

如果 aggregate 单次耗时高且 CPU 高,说明确实存在计算热点;如果耗时高但 CPU 低,则更可能是等待数据库或下游。

26.6 GC 导致 CPU 高

判断证据:

  1. GC 线程 CPU 高;
  2. GC 日志频繁;
  3. Young GC 或并发 GC 周期密集;
  4. 堆使用接近上限;
  5. Live 数据增长。

排查:

jcmd <pid> GC.heap_info
jcmd <pid> VM.flags
jcmd <pid> JFR.start name=cpu settings=profile duration=120s filename=cpu.jfr

常见处理:

原因 处理
分配速率高 减少临时对象、流式处理
堆太小 扩容或降低 Live
Survivor 过小 调整新生代
缓存泄漏 堆 dump 定位
CPU 配额不足 增加 CPU limit
收集器不适合 对比 G1/ZGC

26.7 JIT 编译导致 CPU 高

启动阶段常见 C1 CompilerThreadC2 CompilerThread 使用 CPU。这是 JVM 将热点方法编译成本地代码的正常行为。

判断:

  1. 高 CPU 集中在启动后几分钟;
  2. 稳定后下降;
  3. 请求延迟逐步改善;
  4. 没有异常 GC。

处理方式:

  1. 预热流量逐步放开;
  2. 使用应用启动后健康检查就绪延迟;
  3. 避免 CPU limit 设置过低;
  4. 不要盲目关闭 JIT。

如果是运行中长期大量编译,则可能是方法过多、热点不稳定、缓存失效或类加载异常,需要继续抓 JFR。

26.8 sys CPU 高与线程切换

sys CPU 高通常与系统调用、上下文切换、锁竞争、大量短生命周期线程有关。

观察:

vmstat 1
pidstat -wt -p <pid> 1

关注:

指标 含义
cs 上下文切换
in 中断
r 可运行队列
b 阻塞进程
voluntary/involuntary ctx switch 线程切换类型

处理方向:

  1. 降低线程数量;
  2. 使用虚拟线程时关注阻塞和调度行为;
  3. 减少锁竞争;
  4. 批量化和缓冲 IO;
  5. 检查是否频繁做系统调用;
  6. 保持 CPU limit 与实际负载匹配。

26.9 容器 CPU 场景

容器中要区分:

  1. 宿主机 CPU 使用率;
  2. Pod CPU usage;
  3. CPU limit;
  4. CPU throttling;
  5. JVM 感知的 CPU 数;
  6. GC 线程数。

查看 JVM 感知处理器数:

jcmd <pid> VM.flags | findstr /i "ActiveProcessorCount"

CPU limit 过低会造成:

  1. GC 并发阶段争抢 CPU;
  2. JIT 编译拖慢;
  3. 延迟毛刺;
  4. 应用线程排队。

若 limit 为 2 核,却配置大量业务线程和 GC 线程,操作系统会限制实际执行时间,表现为“代码不慢但请求排队”。

26.10 排查模板

时间:2026-08-25 10:20-10:35
现象:单实例 CPU 95%,P99 从 80ms 到 2s

1. top -H 找到高 CPU 线程
2. 连续采集 3 次线程 dump
3. nid 对应:http-nio-8080-exec-8
4. 栈顶:com.demo.JsonUtils.toJson
5. JFR:JsonUtils 占 65% execution sample
6. 业务日志:该时间段一个导出请求返回 80MB 数据
7. 结论:循环序列化大对象导致 CPU 高
8. 修复:分页流式输出,限制单次导出
9. 验证:压测 CPU 40%,P99 90ms

排查报告要包含证据链,避免只写“代码有性能问题”。

本章小结

CPU 高排查要先判断 user/sys/iowait,再定位到具体线程。业务线程通过线程 dump、JFR 和 Arthas 定位方法;GC 线程回到 GC 日志和堆分析;JIT 线程多见于启动预热。容器环境还必须考虑 CPU limit、throttling 和 JVM 感知处理器数。最终结论要有系统指标、线程栈、事件数据和业务特征的共同支撑。

思考题

  1. user CPU 高和 sys CPU 高的排查方向有何不同?
  2. 为什么 socket read 时 Java 线程可能显示 RUNNABLE?
  3. GC 线程 CPU 高时应该先看哪些证据?
  4. CPU limit 过低为什么会造成延迟毛刺?
  5. 如何证明某个方法是 CPU 高的根因?