这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 CPU 高不是根因,而是现象。JVM 场景下,CPU 高可能来自业务计算、GC、JIT 编译、序列化、正则回溯、锁自旋,也可能是容器 CPU 配额不足导致的一点小负载就打满。
本章给出一套从系统到 Java 方法、再到根因的排查路径。
26.1 先定义问题
排查前先确认四个事实:
- 是 user CPU 高,还是 sys CPU 高;
- 是持续高,还是毛刺;
- 是单实例,还是所有实例;
- 是否伴随延迟、超时、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(...)
这说明热点在正则匹配。继续检查:
- 正则来源是否用户可控;
- 是否存在嵌套量词;
- 输入长度是否过大;
- 是否有超时保护;
- 是否可以换成安全匹配方式。
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 高
判断证据:
- GC 线程 CPU 高;
- GC 日志频繁;
- Young GC 或并发 GC 周期密集;
- 堆使用接近上限;
- 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 CompilerThread、C2 CompilerThread 使用 CPU。这是 JVM 将热点方法编译成本地代码的正常行为。
判断:
- 高 CPU 集中在启动后几分钟;
- 稳定后下降;
- 请求延迟逐步改善;
- 没有异常 GC。
处理方式:
- 预热流量逐步放开;
- 使用应用启动后健康检查就绪延迟;
- 避免 CPU limit 设置过低;
- 不要盲目关闭 JIT。
如果是运行中长期大量编译,则可能是方法过多、热点不稳定、缓存失效或类加载异常,需要继续抓 JFR。
26.8 sys CPU 高与线程切换
sys CPU 高通常与系统调用、上下文切换、锁竞争、大量短生命周期线程有关。
观察:
vmstat 1
pidstat -wt -p <pid> 1
关注:
| 指标 | 含义 |
|---|---|
cs |
上下文切换 |
in |
中断 |
r |
可运行队列 |
b |
阻塞进程 |
| voluntary/involuntary ctx switch | 线程切换类型 |
处理方向:
- 降低线程数量;
- 使用虚拟线程时关注阻塞和调度行为;
- 减少锁竞争;
- 批量化和缓冲 IO;
- 检查是否频繁做系统调用;
- 保持 CPU limit 与实际负载匹配。
26.9 容器 CPU 场景
容器中要区分:
- 宿主机 CPU 使用率;
- Pod CPU usage;
- CPU limit;
- CPU throttling;
- JVM 感知的 CPU 数;
- GC 线程数。
查看 JVM 感知处理器数:
jcmd <pid> VM.flags | findstr /i "ActiveProcessorCount"
CPU limit 过低会造成:
- GC 并发阶段争抢 CPU;
- JIT 编译拖慢;
- 延迟毛刺;
- 应用线程排队。
若 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 感知处理器数。最终结论要有系统指标、线程栈、事件数据和业务特征的共同支撑。
思考题
- user CPU 高和 sys CPU 高的排查方向有何不同?
- 为什么 socket read 时 Java 线程可能显示 RUNNABLE?
- GC 线程 CPU 高时应该先看哪些证据?
- CPU limit 过低为什么会造成延迟毛刺?
- 如何证明某个方法是 CPU 高的根因?