Skip to content

06.5 - CPU飙升排查方法论

定位: 从CPU告警到定位具体代码行的完整排查流程 面试高频度: ⭐⭐⭐⭐⭐

一、为什么CPU飙升是JVM排查的"第一优先级"问题?

CPU飙升直接导致:

  1. 接口响应变慢 — CPU被抢占,业务线程等待
  2. 请求堆积 — 线程池队列增长,最终拒绝服务
  3. 级联故障 — 上游服务超时重试,打垮下游

CPU飙升的排查是"三高问题"(高频、高效、高压)中的最高频——告警系统中最常见的告警之一,排查路径非常清晰,但需要熟练掌握工具链。

二、CPU飙升的四大原因

原因特征典型场景
死循环/密集计算某个线程CPU持续飙高while(true)忘记break、递归无终止条件
Full GCGC线程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%。排查过程如下:

bash
# 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%。

bash
# 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 java

TID=23456 占CPU 62%,是主要嫌疑人。

bash
# Step 3: TID转十六进制
$ printf '%x\n' 23456
5ba0
bash
# 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占比很高。再看代码:

java
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飙高不是业务代码的问题:

bash
# 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步:

bash
# 一键显示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最高的线程和栈。

进一步排查慢方法:

bash
# 追踪方法调用耗时
trace com.example.service.ProductCache get

详细Arthas用法见 **图形化工具(JVisualVM与Arthas)**。

六、CPU飙升的进阶排查场景

6.1 上下文切换高(sys CPU高)

bash
# 查看上下文切换
$ 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编译风暴

bash
# 检查Code Cache
jinfo -flag ReservedCodeCacheSize <pid>
# 或
jstat -compiler <pid>

# Code Cache满了 → JIT不再编译 → 解释执行 → CPU飙升
# -XX:+PrintCodeCache 查看使用率
# 如果Code Cache已满:
#   增大 -XX:ReservedCodeCacheSize
#   检查是否启用了 -XX:-TieredCompilation(关闭分层编译)

6.3 正则回溯爆炸

java
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 &lt;pid&gt;找到CPU最高的线程TID → printf '%x\n' &lt;tid&gt;转十六进制 → jstack &lt;pid&gt; | grep -A 30 &lt;hex&gt;定位到代码行。更快的方案直接用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堆** - 堆结构

Knowledge4J — Java 知识库