06.5 - CPU飙升排查方法论
定位: 从CPU告警到定位具体代码行的完整排查流程 面试高频度: ⭐⭐⭐⭐⭐
一、为什么CPU飙升是JVM排查的"第一优先级"问题?
CPU飙升直接导致:
- 接口响应变慢 — CPU被抢占,业务线程等待
- 请求堆积 — 线程池队列增长,最终拒绝服务
- 级联故障 — 上游服务超时重试,打垮下游
CPU飙升的排查是"三高问题"(高频、高效、高压)中的最高频——告警系统中最常见的告警之一,排查路径非常清晰,但需要熟练掌握工具链。
二、CPU飙升的四大原因
| 原因 | 特征 | 典型场景 |
|---|---|---|
| 死循环/密集计算 | 某个线程CPU持续飙高 | while(true)忘记break、递归无终止条件 |
| Full GC | GC线程CPU高 + 业务线程暂停 | 内存泄漏 → GC thrashing |
| JIT编译风暴 | CompilerThread CPU高 | Code Cache满了 → 编译线程反复尝试 |
| 锁竞争/上下文切换 | CPU不高但sys CPU高 | 锁粒度太大 → 大量线程在争抢 |
| 正则/序列化回溯 | 偶发性CPU飚升 | 复杂正则匹配失败 → 回溯爆炸 |
三、CPU排查四步法(经典流程)
┌─────────────────────────────────────────────────────────────────────┐
│ CPU飙升四步排查法 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Step 1: 找到Java进程 │
│ ┌─────────────────────────────────────────────┐ │
│ │ $ top │ │
│ │ PID USER PR NI VIRT RES SHR │ │
│ │ 12345 app 20 0 4.0g 1.2g 50m S │ ← 高CPU的进程 │
│ │ %CPU = 98.5 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Step 2: 找到CPU最高的线程 │
│ ┌─────────────────────────────────────────────┐ │
│ │ $ top -H -p 12345 │ │
│ │ PID USER PR NI VIRT RES SHR │ │
│ │ 23456 app 20 0 4.0g 1.2g 50m R │ ← 线程级CPU最高 │
│ │ TID=23456, 占进程CPU的62% │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Step 3: 将线程TID转为十六进制 │
│ ┌─────────────────────────────────────────────┐ │
│ │ $ printf '%x\n' 23456 │ │
│ │ 5ba0 │ ← jstack中的nid │
│ └─────────────────────────────────────────────┘ │
│ │
│ Step 4: 在jstack中找到对应线程 │
│ ┌─────────────────────────────────────────────┐ │
│ │ $ jstack 12345 | grep -A 30 '0x5ba0' │ │
│ │ "http-nio-8080-exec-18" #85 daemon │ │
│ │ java.lang.Thread.State: RUNNABLE │ │
│ │ at com.example.service.Processor.process │ │
│ │ at com.example.service.Processor.loop() │ ← 定位到这里 │
│ │ - locked <0x000...> │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 这四步之后,你就在代码中找到了CPU消耗的具体位置。 │
│ 然后分析这段代码为什么那么耗CPU即可。 │
│ │
└─────────────────────────────────────────────────────────────────────┘四、实战案例:一次完整的CPU排查
场景
收到告警:订单服务CPU 98%。排查过程如下:
# Step 1: top确认
$ top
top - 10:15:30 up 3 days, 2:15, 2 users, load average: 15.0, 10.0, 5.0
Tasks: 120 total, 1 running, 119 sleeping
%Cpu(s): 95.2 us, 3.5 sy, 1.3 ni, 0.0 id, 0.0 wa
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12345 app 20 0 4.000g 1.200g 50000 R 98.5 15.0 123:45.67 java确认:PID 12345,Java进程CPU 98.5%。
# Step 2: 找到最高CPU线程
$ top -H -p 12345
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
23456 app 20 0 4.000g 1.200g 50000 R 62.0 15.0 80:30.00 java
23457 app 20 0 4.000g 1.200g 50000 S 15.0 15.0 20:00.00 java
23458 app 20 0 4.000g 1.200g 50000 S 10.0 15.0 15:00.00 javaTID=23456 占CPU 62%,是主要嫌疑人。
# Step 3: TID转十六进制
$ printf '%x\n' 23456
5ba0# Step 4: jstack定位
$ jstack 12345 | grep -A 30 '0x5ba0'
"http-nio-8080-exec-18" #85 daemon prio=5 os_prio=0 tid=0x00007f3c900b8000 nid=0x5ba0 runnable [0x00007f3c8e5f4000]
java.lang.Thread.State: RUNNABLE
at java.util.HashMap.get(HashMap.java:561) ← HashMap.get()?
at com.example.service.ProductCache.get(ProductCache.java:35)
at com.example.service.ProductService.getProduct(ProductService.java:22)
at com.example.controller.ProductController.get(ProductController.java:15)
...看到HashMap.get()在CPU占比很高。再看代码:
public class ProductCache {
// 检查: 是否用了线程安全的ConcurrentHashMap?
private Map<String, Product> cache = new HashMap<>(); // ❌ 非线程安全
public Product get(String id) {
return cache.get(id); // 多线程下HashMap会形成循环链表 → get()死循环
}
}根因: HashMap在多线程put时形成循环链表,导致get()陷入死循环。
修复: 改为ConcurrentHashMap。
另一个场景: 居然是Full GC
有时CPU飙高不是业务代码的问题:
# jstack看到的是GC线程在跑
$ jstack 12345 | grep -A 10 'GC'
"G1 Young Thread" #10 os_prio=0 cpu=0.50ms
"G1 Conc#1" #11 os_prio=0 cpu=50.00ms ← 并发标记线程CPU高
"G1 Conc#2" #12 os_prio=0 cpu=45.00ms
# GC线程CPU高说明GC频繁,进一步检查jstat
$ jstat -gcutil 12345 1000
S0 S1 E O M YGC YGCT FGC FGCT GCT
0.00 0.00 98.0 99.0 85.0 5000 30.0 100 50.0 80.0老年代99%,FGC=100次!CPU飙升的真正原因是Full GC thrashing——GC线程在疯狂回收但回收不了多少。
详细排查方法见 **FullGC频繁排查方法论**。
五、Arthas的CPU排查方案(更快)
用传统方法需要4步,用Arthas只要1步:
# 一键显示CPU最高的3个线程及其栈
$ thread -n 3
"C1 CompilerThread0" [Internal] cpuUsage=62%
...
"http-nio-8080-exec-18" Id=85 cpuUsage=15%
at com.example.service.ProductCache.get
...
"VM Periodic Task Thread" [Internal] cpuUsage=8%Arthas优势: 省去了
top -H -p找TID、TID转十六进制、jstack grep三步,直接输出CPU最高的线程和栈。
进一步排查慢方法:
# 追踪方法调用耗时
trace com.example.service.ProductCache get详细Arthas用法见 **图形化工具(JVisualVM与Arthas)**。
六、CPU飙升的进阶排查场景
6.1 上下文切换高(sys CPU高)
# 查看上下文切换
$ pidstat -w -p <pid> 1
PID cswch/s nvcswch/s Command
12345 15000 12000 java
# cswch/s: 自愿上下文切换(线程等待I/O)
# nvcswch/s: 非自愿上下文切换(时间片用完/优先级抢占)非自愿上下文切换高 → 太多线程竞争CPU → 线程数过多或锁竞争。
6.2 JIT编译风暴
# 检查Code Cache
jinfo -flag ReservedCodeCacheSize <pid>
# 或
jstat -compiler <pid>
# Code Cache满了 → JIT不再编译 → 解释执行 → CPU飙升
# -XX:+PrintCodeCache 查看使用率
# 如果Code Cache已满:
# 增大 -XX:ReservedCodeCacheSize
# 检查是否启用了 -XX:-TieredCompilation(关闭分层编译)6.3 正则回溯爆炸
String regex = "(a*)*b"; // ❌ 灾难性回溯
String input = "aaaaaaaaac"; // 匹配失败时回溯极多
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
boolean result = matcher.matches(); // CPU 100%!这种场景通过thread -n看到栈顶在Pattern/Matcher相关方法。
七、CPU排查工具箱
| 工具 | 用途 | 效率 |
|---|---|---|
top | 找到高CPU进程 | ⭐⭐⭐ |
top -H -p | 找到高CPU线程 | ⭐⭐⭐ |
printf '%x\n' | TID转十六进制 | ⭐⭐ |
jstack | 查看线程栈 | ⭐⭐⭐ |
thread -n 3 (Arthas) | 一键找CPU热点 | ⭐⭐⭐⭐⭐ |
thread -b (Arthas) | 检查死锁 | ⭐⭐⭐⭐⭐ |
profiler (Arthas) | 火焰图 | ⭐⭐⭐⭐ |
pidstat -w | 上下文切换 | ⭐⭐⭐ |
perf top -p (Linux) | 系统级采样 | ⭐⭐⭐⭐ |
八、面试视角
高频面试题
| 面试题 | 考察点 | 回答要点 |
|---|---|---|
| "CPU飙升怎么排查?" | 基础方法论 | top → top -H -p → printf %x → jstack 四步法 |
| "top -H -p的作用?" | 工具理解 | 显示进程内各线程的CPU使用率 |
| "Arthas怎么排查CPU问题?" | Arthas掌握 | thread -n 3 直接看,profiler generate 火焰图 |
| "CPU高一定是死循环吗?" | 排查广度 | 不一定。Full GC、JIT编译、上下文切换、正则回溯都可能导致CPU高 |
| "你遇到过什么CPU问题?" | 实战经验 | 举例:HashMap死循环、GC thrashing、正则回溯 |
| "jstack看到GC线程CPU高怎么办?" | 关联分析 | 这是GC频繁导致的,转查GC日志和jstat |
| "线程上下文切换高说明什么?" | 系统理解 | 线程数过多、锁竞争激烈或CPU时间分片过小 |
面试回答模板
Q: 线上CPU了,你怎么排查? A: 核心四步:
top找高CPU的Java进程PID →top -H -p <pid>找到CPU最高的线程TID →printf '%x\n' <tid>转十六进制 →jstack <pid> | grep -A 30 <hex>定位到代码行。更快的方案直接用Arthas的thread -n 3一步到位。
Q: 你遇到过最复杂的CPU飙升场景是什么? A: 一次线上的坑是HashMap死循环。现象是某个线程CPU稳定在60%以上。jstack看到是
HashMap.get()。代码里用的不是ConcurrentHashMap,多线程并发put导致HashMap内部形成环形链表,get()无限循环。修复后CPU从98%降到15%。
📚 相关链接
- ← 返回 **调优索引**
- **常用工具(jps与jstat与jmap与jstack与jinfo)** - jstack详解
- **图形化工具(JVisualVM与Arthas)** - Arthas thread/profiler
- **FullGC频繁排查方法论** - GC导致的CPU飙升
- **OOM排查方法论** - 内存泄漏导致GC thrashing
- **Java堆** - 堆结构