Linux 服务器 CPU 飙升排查:定位 Java 程序问题线程
大家好,我是joker,希望你快乐。
线上服务器 CPU 突然飙到 100%,告警疯狂响,怎么办?别慌,本质上就三步:找到进程 → 找到线程 → 找到代码。本文从原理到实战,完整梳理这套排查方法论。
CPU 飙高的常见原因
在动手排查之前,先了解 Java 程序 CPU 飙高的常见原因,有助于快速缩小排查范围:
| 原因 | 特征 | 典型场景 |
|---|---|---|
| 死循环 | 单线程持续占满一个 CPU 核心 | while 条件判断失误、循环变量未更新 |
| 正则回溯(ReDoS) | 单线程 CPU 100%,堆栈卡在正则匹配 | 恶意正则 + 特殊输入,如 (a+)+ 匹配 aaaaaaaaab |
| 频繁 GC | GC 线程占用高,业务线程也慢 | 内存泄漏导致 Full GC 频繁,或堆内存设置过小 |
| 锁竞争(上下文切换) | 多线程 RUNNABLE/BLOCKED 频繁切换,整体 CPU 高但单线程不高 | synchronized/ReentrantLock 竞争激烈 |
| 线程数爆炸 | 大量线程 TIMED_WAITING,load average 飙高 | CachedThreadPool 无界创建线程 |
其中,死循环和正则回溯是最常见的"单线程打满 CPU"场景,也是本文排查步骤的主要目标。频繁 GC 需要配合 jstat 排查,锁竞争和线程数爆炸需要分析线程状态分布。
排查总览:五步定位法
完整的排查链路:
top 定位进程 → top -Hp 定位线程 → printf 转十六进制 → jstack 导出堆栈 → 定位代码行

核心逻辑:Linux 的线程 ID 是十进制,而 jstack 输出中的 nid 是十六进制,两者需要对应起来才能找到问题线程的堆栈。
排查步骤详解
步骤一:定位高 CPU 的 Java 进程
top
输出示例:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12345 app 20 0 4096m 512m 128m S 198.5 12.3 5:32.18 java
987 root 20 0 2048m 256m 64m S 2.1 6.1 0:12.05 nginx
关注点:
- %CPU:多核机器上可能超过 100%(如 4 核机器最高 400%),198.5% 表示占满了约 2 个核心
- COMMAND:确认是 Java 进程,记录 PID(如 12345)
如果机器上运行了多个 Java 进程,可以通过 -c 选项显示完整命令行,方便区分:
top -c
步骤二:定位高 CPU 的线程
top -Hp 12345
-H 表示 Threads mode(H = Thread),展示进程内各线程的资源占用;-p 表示 PID mode(p = Process ID),只监控指定进程。两者组合就是"只看这个进程的各线程情况"。
输出示例:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12367 app 20 0 4096m 512m 128m S 99.8 3.1 2:45.12 java
12368 app 20 0 4096m 512m 128m S 98.5 3.1 2:43.89 java
12345 app 20 0 4096m 512m 128m S 0.3 3.1 0:01.23 java
12346 app 20 0 4096m 512m 128m S 0.1 3.1 0:00.56 java
注意:-H 模式下 PID 列显示的是 TID(Thread ID,即 LWP),不是进程 ID。可以看到线程 12367 和 12368 各占满了一个 CPU 核心,它们就是问题线程。记录这两个 TID。
按 Shift + P 可以按 CPU 使用率降序排列,方便找到最耗 CPU 的线程。
步骤三:线程 ID 十六进制转换
jstack 输出中的线程 ID(nid)是十六进制的,需要将十进制的 TID 转换:
printf "%x\n" 12367
# 输出:304f
printf "%x\n" 12368
# 输出:3050
为什么需要转换? 因为 Linux 内核的线程 ID(TID/LWP)是十进制,而 JVM 的 jstack 工具输出中使用十六进制的 nid(Native Thread ID)来标识操作系统线程。两者是同一个东西,只是进制不同,必须转换后才能对应。
注意:printf 输出的是小写十六进制,jstack 的 nid 也是小写,直接匹配即可。
步骤四:jstack 导出线程堆栈
jstack 12345 > jstack.log
然后在输出中搜索对应的 nid:
grep -A 30 "nid=0x304f" jstack.log
输出示例:
"busi-worker-thread-1" #12 prio=5 os_prio=0 cpu=165231.89ms elapsed=276.45s tid=0x00007f8d88012345 nid=0x304f runnable [0x00007f8d7c3a0000]
java.lang.Thread.State: RUNNABLE
at com.example.service.OrderService.processOrder(OrderService.java:45)
at com.example.service.OrderService.lambda$handle$0(OrderService.java:32)
at com.example.service.OrderService$$Lambda$123/0x0000000800c00a08.run(Unknown Source)
at java.lang.Thread.run(Thread.java:833)
关键信息解读:
| 字段 | 含义 |
|---|---|
"busi-worker-thread-1" |
线程名,好的命名习惯能快速定位业务模块 |
#12 |
JVM 内部线程编号 |
prio=5 |
线程优先级(默认 5) |
cpu=165231.89ms |
累计占用 CPU 时间,异常高的值是问题线索 |
tid=0x00007f8d88012345 |
JVM 内部线程 ID |
nid=0x304f |
操作系统线程 ID(十六进制),与 top -Hp 中的 TID 对应 |
runnable |
线程状态 |
OrderService.java:45 |
问题代码位置 |
步骤五:定位代码行
根据堆栈中的 OrderService.java:45,打开源码找到第 45 行,结合业务逻辑分析根因。
多次 jstack 对比技巧:如果一次 jstack 不确定,可以间隔 3-5 秒执行两到三次:
jstack 12345 > jstack_1.log
sleep 3
jstack 12345 > jstack_2.log
sleep 3
jstack 12345 > jstack_3.log
如果同一线程在多次快照中堆栈一致(特别是最顶层的几帧不变),说明线程卡在同一个位置,大概率是死循环或阻塞。
jstack 线程状态解读
jstack 输出中线程的状态是判断问题类型的关键依据。Java 线程有 6 种状态(定义在 java.lang.Thread.State 枚举中):
| 状态 | 含义 | CPU 飙高排查中的意义 |
|---|---|---|
| RUNNABLE | 线程正在执行或等待 CPU 调度 | 最值得关注,持续 RUNNABLE 且 CPU 高 → 死循环、正则回溯、密集计算 |
| BLOCKED | 线程等待获取监视器锁(synchronized) | 多个线程 BLOCKED → 锁竞争激烈,可能导致上下文切换飙升 |
| WAITING | 无限期等待(Object.wait()、LockSupport.park()) | 通常是正常的等待状态,如线程池空闲线程等待任务 |
| TIMED_WAITING | 有超时的等待(Thread.sleep()、wait(timeout)) | 通常是正常状态,但如果大量线程 TIMED_WAITING → 可能线程数爆炸 |
| NEW | 线程已创建但未启动 | 一般不关注 |
| TERMINATED | 线程已终止 | 一般不关注 |
重要提示:RUNNABLE 状态在 JVM 层面包含两种情况——正在执行和等待 CPU 调度。也就是说,一个处于 RUNNABLE 状态的线程不一定在消耗 CPU,也可能在等待 CPU 时间片。需要结合 top -Hp 的 CPU 占用率来判断。
锁信息解读(jstack -l 输出):
- locked <0x000000076ab20c90> (a java.lang.Object) ← 线程持有的锁
- waiting to lock <0x000000076ab20c90> (a java.lang.Object) ← 线程等待的锁
通过锁地址可以串联起"谁持有锁 → 谁在等锁"的关系链,定位锁竞争的根源。
实战案例
案例一:死循环导致 CPU 飙高
模拟代码:
public class DeadLoopDemo {
public static void main(String[] args) {
new Thread(() -> {
int count = 0;
while (true) {
count++;
}
}, "dead-loop-thread").start();
}
}
排查过程:
# 步骤1:top 找到 Java 进程
top
# PID 23456 CPU 99.8% java
# 步骤2:top -Hp 找到高 CPU 线程
top -Hp 23456
# TID 23478 CPU 99.5% java
# 步骤3:转十六进制
printf "%x\n" 23478
# 5ba6
# 步骤4:jstack 定位堆栈
jstack 23456 | grep -A 20 "nid=0x5ba6"
jstack 输出:
"dead-loop-thread" #11 prio=5 os_prio=0 cpu=98765.32ms elapsed=120.45s tid=0x00007f8d88054321 nid=0x5ba6 runnable [0x00007f8d7c3b0000]
java.lang.Thread.State: RUNNABLE
at DeadLoopDemo.lambda$main$0(DeadLoopDemo.java:5)
at DeadLoopDemo$$Lambda$1/0x0000000800c00a08.run(Unknown Source)
at java.lang.Thread.run(Thread.java:833)
定位:DeadLoopDemo.java:5 就是 while (true) 那一行,死循环实锤。
修复:添加退出条件或最大循环次数限制。
案例二:正则回溯(ReDoS)导致 CPU 飙高
正则表达式回溯是线上 CPU 飙高的隐蔽杀手。某些正则表达式在匹配特定输入时,会产生指数级的回溯尝试。
模拟代码:
public class ReDoSDemo {
private static final Pattern EVIL_PATTERN = Pattern.compile("(a+)+b");
public static void main(String[] args) {
new Thread(() -> {
// 单次回溯执行完线程就退出,CPU 飙升只持续一瞬间,无法用 top/jstack 观察
// 必须用 while(true) 持续触发,才能让 CPU 持续打满
String input = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaac";
while (true) {
EVIL_PATTERN.matcher(input).matches();
}
}, "regex-thread").start();
}
}
这个正则 (a+)+b 看起来简单,但匹配 aaa...ac(以 c 结尾而不是 b)时,引擎会尝试所有可能的分组方式,产生指数级回溯。
关于输入长度:回溯耗时随 a 的数量指数增长(每多 1 个 a 耗时翻倍),且受 CPU 速度、JDK 版本影响极大。实测参考(Java 8、普通笔记本):
a 的数量 |
单次匹配耗时 |
|---|---|
| 20 | ~80ms |
| 24 | ~400ms |
| 26 | ~1.5s |
| 28 | ~6s |
| 30 | ~24s |
| 33 | ~191s |
上面示例用 30 个 a(单次约 24 秒)配合 while(true),既保证 CPU 持续 100%,又方便在匹配过程中从容执行 top、jstack 等命令。
常见误区:
- 漏掉
while(true):单次matches()无论多快都会结束,线程随即退出,CPU 瞬间降回 0。看到"程序很快结束"先检查这里,而不是急着加长输入。 - 在
@Test方法里跑:JUnit 测试方法返回后框架会调用System.exit退出 JVM,不会等子线程。建议用main方法运行,或在测试方法里用thread.join()等待。
排查过程:
top -Hp 34567
# TID 34589 CPU 99.7% java
printf "%x\n" 34589
# 871d
jstack 34567 | grep -A 20 "nid=0x871d"
jstack 输出:
"regex-thread" #12 prio=5 os_prio=0 cpu=56432.11ms elapsed=67.23s tid=0x00007f8d88065432 nid=0x871d runnable [0x00007f8d7c3c0000]
java.lang.Thread.State: RUNNABLE
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4803)
at java.util.regex.Pattern$Loop.match(Pattern.java:4845)
at java.util.regex.Pattern$GroupTail.match(Pattern.java:4870)
at java.util.regex.Pattern$Curly.match(Pattern.java:4542)
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4803)
at java.util.regex.Pattern$Branch.match(Pattern.java:4785)
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4803)
at java.util.regex.Pattern$Loop.match(Pattern.java:4845)
at java.util.regex.Pattern$GroupTail.match(Pattern.java:4870)
at java.util.regex.Pattern$Curly.match(Pattern.java:4542)
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4803)
at java.util.regex.Pattern$Branch.match(Pattern.java:4785)
at java.util.regex.Pattern.match(Pattern.java:1674)
at java.util.regex.Matcher.match(Matcher.java:396)
at java.util.regex.Matcher.matches(Matcher.java:615)
at ReDoSDemo.lambda$main$0(ReDoSDemo.java:10)
at java.lang.Thread.run(Thread.java:833)
特征识别:堆栈中出现大量 Pattern$Loop.match、Pattern$Curly.match、Pattern$GroupHead.match 的递归调用,这是正则回溯的典型特征。
修复方案:
- 使用占有型量词(Possessive Quantifier):
(a++)+b,占有型量词匹配后不回退 - 使用原子组(Atomic Grouping):
(?>a+)+b - 限制输入长度,避免超长字符串触发回溯
- 使用超时机制:Java 9+ 支持
matcher.matches()配合Matcher.usePattern()和超时控制
案例三:频繁 GC 导致 CPU 飙高
这种场景比较特殊:top -Hp 找不到明显的高 CPU 线程,但进程整体 CPU 很高。
现象:
top -Hp 45678
# 没有单个线程 CPU 特别高,但多个 GC 线程加起来占用很高
# "GC-Thread-0" CPU 15%
# "GC-Thread-1" CPU 14%
# "GC-Thread-2" CPU 13%
# ...
排查方法:用 jstat 观察 GC 频率:
jstat -gc 45678 1000 5
输出示例:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT
512.0 512.0 0.0 448.0 8192.0 1024.0 16384.0 16012.3 10240.0 9876.5 1024.0 945.6 1256 12.345 89 45.678 0 0.000 58.023
512.0 512.0 128.0 0.0 8192.0 2048.0 16384.0 16045.6 10240.0 9876.5 1024.0 945.6 1257 12.356 90 45.789 0 0.000 58.145
512.0 512.0 0.0 256.0 8192.0 3072.0 16384.0 16078.9 10240.0 9876.5 1024.0 945.6 1258 12.367 91 45.901 0 0.000 58.268
关注点:
- FGC(Full GC Count):1 秒内 Full GC 次数从 89 → 90 → 91,几乎每秒一次 Full GC
- OU(Old Used):老年代使用量持续在 16000+,接近老年代容量 16384,内存快满了
- GCT(GC Total Time):GC 累计时间持续增长
确认方法:jstack 中搜索 GC 线程:
jstack 45678 | grep -A 5 "GC"
如果 GC 线程频繁出现且处于 RUNNABLE 状态,配合 jstat 的数据,可以确认是频繁 GC 导致的 CPU 飙高。
根因:通常是内存泄漏导致老年代被占满,触发频繁 Full GC。需要用 jmap 导出堆内存进一步分析:
jmap -dump:format=b,file=heap_dump.hprof 45678
然后用 MAT(Memory Analyzer Tool)或 VisualVM 分析堆转储文件,找出占用内存最大的对象。
进阶工具与技巧
jstack -l:额外输出锁信息
jstack -l 12345 > jstack_with_locks.log
-l 选项会在每个线程堆栈后附加锁信息:
Locked ownable synchronizers:
- <0x000000076ab20c90> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
这在排查锁竞争和死锁时非常有用。
jstack -F:强制导出
当 JVM 无响应(如死锁导致进程假死),常规 jstack 可能无法连接,此时使用 -F 强制导出:
jstack -F 12345 > jstack_force.log
注意:-F 模式可能丢失部分信息(如锁信息),且会导致 JVM 短暂停顿(STW),生产环境谨慎使用。
Arthas:一键定位 Top N 线程
Arthas 是阿里巴巴开源的 Java 诊断工具,可以替代 top -Hp + printf + jstack 的繁琐步骤,一条命令定位问题。
安装与启动:
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
启动后选择目标 Java 进程即可。
thread -n 3:查看 CPU 占用最高的 3 个线程
thread -n 3
输出示例:
"dead-loop-thread" Id=12 cpuUsage=99.5% deltaTime=201ms time=98765ms RUNNABLE
at com.example.service.OrderService.processOrder(OrderService.java:45)
at com.example.service.OrderService.lambda$handle$0(OrderService.java:32)
at java.lang.Thread.run(Thread.java:833)
"regex-thread" Id=13 cpuUsage=85.2% deltaTime=172ms time=56432ms RUNNABLE
at java.util.regex.Pattern$Loop.match(Pattern.java:4845)
at java.util.regex.Pattern$GroupHead.match(Pattern.java:4803)
...
"http-nio-8080-exec-5" Id=24 cpuUsage=12.3% deltaTime=25ms time=3456ms RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
...
一条命令就完成了 top -Hp + printf + jstack + grep 的全部工作,直接看到线程名、CPU 占用率、线程状态和堆栈。
thread -b:查找阻塞其他线程的线程
thread -b
直接找出持有锁导致其他线程阻塞的"罪魁祸首"线程。
thread --state BLOCKED:按状态筛选线程
thread --state BLOCKED
只显示 BLOCKED 状态的线程,快速定位锁竞争问题。
dashboard:实时系统面板
dashboard
实时展示线程、内存、GC 等核心指标,每 5 秒刷新一次,适合快速了解系统整体状况。
profiler:生成 CPU 火焰图
profiler start
# 等待 30 秒采集数据
profiler stop --format html
生成火焰图,直观展示 CPU 时间花在哪些方法上。火焰图中"平顶山"越宽的方法,占用的 CPU 时间越多。
vmstat:观察上下文切换(系统命令,非 Arthas)
如果怀疑是锁竞争导致的频繁上下文切换,用系统自带的 vmstat 观察(注意 vmstat 是 Linux procps-ng 包提供的系统命令,不是 Arthas 的功能,需要单独安装):
vmstat 1 5
输出示例:
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 123456 12345 234567 0 0 12 15 120 85000 45 55 0 0 0
3 0 0 123400 12345 234567 0 0 10 12 115 92000 42 58 0 0 0
关注 cs(context switch)列:如果 cs 值异常高(如超过 100 万/秒),说明线程上下文切换频繁,可能是锁竞争或线程数过多导致。
排查流程速查表
| 步骤 | 命令 | 说明 |
|---|---|---|
| 1. 定位进程 | top |
找 %CPU 最高的 Java 进程,记 PID |
| 2. 定位线程 | top -Hp <PID> |
找 %CPU 最高的线程,记 TID |
| 3. 转十六进制 | printf "%x\n" <TID> |
转换为 jstack 可匹配的 nid |
| 4. 导出堆栈 | jstack <PID> > jstack.log |
生成线程快照 |
| 5. 定位代码 | grep -A 30 "nid=0xXXX" jstack.log |
找到问题线程的堆栈和代码行 |
| 6. 多次对比 | 间隔 3-5 秒执行 2-3 次 jstack | 确认线程是否持续卡在同一位置 |
| 7. 排查 GC | jstat -gc <PID> 1000 5 |
排除频繁 GC 导致的 CPU 飙高 |
| 8. Arthas 快捷 | thread -n 3 |
一条命令替代步骤 2-5 |
口诀:top → top -Hp → printf → jstack → grep,30 秒定位问题线程。
参考文档
来源:http://www.cnblogs.com/Crazy_Joker
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

浙公网安备 33010602011771号