某台机器CPU飙到100%,top进去看到java进程——怎么一步步定位到哪行代码?
本文目录
先说结论
CPU 高的四种典型原因:业务计算密集(正常)、死循环/热点代码(BUG)、频繁 GC(堆太小或内存泄漏)、锁争用导致自旋。不同原因排查路径不一样。
定位链路(5步走):① `top` 找 pid;② `top -H -p
容器环境的坑:容器内 top 看到的 CPU 可能不准(老版本)、/proc/stat 反映的是宿主机,要结合 cgroup 看。JDK 8u131 以前不认 cgroup,会拿到宿主机的 CPU 数。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
这是Linux + Java 面试最高频的一道题,几乎每次面大数据岗都会被问。面试官想听的不是 '我 top 一下看看',而是一套从操作系统到 JVM 到代码的完整链路:top 定位进程 → top -H 定位线程 → printf %x 转十六进制 → jstack 找堆栈 → 看业务代码。说清楚这条链路的候选人,基本就过了 Linux 那一关。
业务场景:Flink TaskManager CPU 100%,Checkpoint 全失败
我在某元宇宙社交应用做实时数仓的时候,有一次早上 8 点告警,一个实时作业的 Flink TaskManager CPU 长时间 100%,Checkpoint 开始超时失败,背压告警也同步红了。当时数据延迟已经到了 20 分钟,业务方催得紧。我用下面这套流程 15 分钟定位到了根因:一段 UserAgent 解析正则表达式写错,触发了灾难性回溯(ReDoS),某几条异常流量让单个线程死在那段正则上。
# 1. top 看 CPU 最高的进程
top -c
# PID USER %CPU %MEM COMMAND
# 23451 flink 398 42 java -Xmx8g ...TaskManagerRunner
# 2. 定位到进程后,看是哪个线程
top -H -p 23451
# PID USER %CPU ...
# 23587 flink 99.3 <-- 问题线程
# 23588 flink 99.1 <-- 问题线程
# 3. 十进制 tid 转十六进制
printf "%x\n" 23587
# 5c23
# 4. jstack 取堆栈,搜 nid=0x5c23
jstack 23451 > /tmp/jstack.log
grep -A 30 'nid=0x5c23' /tmp/jstack.log
jstack 输出里那个线程的堆栈直接指向了 `java.util.regex.Pattern$Loop.match`,我心里就有数了:正则灾难性回溯。翻业务代码找到那段 UserAgent 解析的正则,改成预编译 + 长度限制,重启 TM 就恢复了。
手段一:top / top -H 基本功
top 其实有很多隐藏用法,新人只会看第一屏,老手会按 `1` 看每个 CPU 核心、按 `H` 切换线程视图、按 `P` 按 CPU 排序、按 `M` 按内存排序。
# top 常用快捷键
# 1 - 展开每个 CPU 核心
# H - 切换到线程视图(等价于 top -H)
# P - 按 CPU 排序(默认)
# M - 按内存排序
# c - 显示完整命令行
# k - 输入 pid 直接 kill
# W - 保存当前设置到 ~/.toprc
# 持续采样(排查间歇性 CPU 高峰必备)
top -b -n 10 -d 1 > /tmp/top.log # 每秒采样一次,采 10 次
# 只看指定进程的线程
top -H -p $(pgrep -f 'TaskManager')
# 查看某个进程已经占了多少 CPU 秒
ps -p 23451 -o pid,etime,time,pcpu,cmd
手段二:pidstat 更精确的持续监控
pidstat 是 sysstat 包里的工具,比 top 更适合做持续监控和复盘。它能给你每一秒的精确快照,还能分别看用户态和内核态的 CPU 占用。
# 每 1 秒采样一次,采 10 次,看某个进程
pidstat -u -p 23451 1 10
# -t 带上线程级别
pidstat -u -t -p 23451 1 10
# %usr 高 = 业务代码热;%system 高 = 系统调用/IO 频繁;%guest 高 = 虚拟化
# 同时看 IO 和上下文切换
pidstat -u -d -w -p 23451 1 10
# %usr %system kB_rd/s kB_wr/s cswch/s nvcswch/s
# cswch = 自愿上下文切换(等 IO/锁)
# nvcswch = 非自愿切换(被抢占,CPU 饱和的标志)
重点:如果 nvcswch(非自愿上下文切换)飙高,说明 CPU 已经满载了,业务在抢 CPU;cswch 高说明线程频繁等待(锁或 IO)。这个细节面试官特别喜欢问。
手段三:jstack 分析线程堆栈
jstack 是 Java CPU 问题的终极武器。它 dump 一瞬间所有线程的堆栈,你拿来就能看每个线程在干什么。经验是连续 dump 3-5 次,间隔 5 秒,这样能看到'哪些线程持续在同一个位置'——持续在热点代码上的就是 CPU 杀手。
# 基本用法
jstack 23451 > /tmp/jstack_1.log
# 进程启动时加了 -XX:+HeapDumpOnOutOfMemoryError 而 jstack 没权限怎么办
sudo -u flink jstack 23451
# 或者用 jcmd(JDK 8+)
jcmd 23451 Thread.print > /tmp/threads.log
# 连续采样 5 次(推荐)
for i in 1 2 3 4 5; do
jstack 23451 > /tmp/jstack_$i.log
sleep 5
done
# 自动化脚本:一条命令从 CPU 高定位到线程堆栈
PID=$(pgrep -f TaskManager)
TID=$(top -b -H -n 1 -p $PID | awk 'NR>7' | sort -rk9 | head -1 | awk '{print $1}')
NID=$(printf '%x' $TID)
jstack $PID | grep -A 30 "nid=0x$NID"
解读要点:堆栈里每个线程会有 `java.lang.Thread.State` 标记,`RUNNABLE` 才是在真正跑的线程,`WAITING/TIMED_WAITING/BLOCKED` 都不吃 CPU。找 CPU 高的线程只看 RUNNABLE 的。
手段四:async-profiler / perf(高阶)
jstack 只是快照,async-profiler 能出火焰图,看到真正的热点代码占 CPU 多少百分比。生产上排查性能问题,火焰图比 jstack 直观 10 倍。
# async-profiler 采样 30 秒出火焰图
./profiler.sh -d 30 -f /tmp/flame.html 23451
# 只看 CPU(默认是 CPU + alloc)
./profiler.sh -d 30 -e cpu -f /tmp/cpu.html 23451
# 非 Java 程序用 perf top
perf top -p 23451
# Python 用 py-spy
py-spy top --pid 23451
py-spy dump --pid 23451
CPU 高的四种原因对比
| 原因 | 现象特征 | 排查工具 | 解决思路 |
|---|---|---|---|
| 业务计算密集 | usr 高 sys 低,多线程都在跑 | pidstat、jstack | 横向扩容或算法优化 |
| 死循环/热点代码 | 少数线程 100%,堆栈停在同一行 | jstack 连续采样 | 改代码 |
| 频繁 Full GC | usr 飙高,jstat 看 GC 频繁 | jstat -gcutil、jmap | 加堆或修泄漏 |
| 锁争用 | cswch 高、BLOCKED 线程多 | jstack 找 BLOCKED | 减锁粒度或换无锁 |
面试高频追问
追问一:怎么区分 CPU 高是 GC 导致的还是业务代码?
用 `jstat -gcutil
追问二:容器里 top 看到 CPU 100%,但宿主机压力不大,有没有坑?
有大坑。JDK 8u131 以前版本不认 cgroup,JVM 会拿到宿主机的 CPU 数,如果宿主机 48 核,它就认为自己有 48 核,`Runtime.getRuntime().availableProcessors()` 返回 48,于是线程池开一堆线程,实际 container 只给 4 核,就各种抢。修复是升 JDK 或者加 `-XX:ActiveProcessorCount=4` 手动指定。另外老版本 Docker 的 top 看的是宿主机 `/proc/stat`,不是容器自己的,看 CPU 使用率得结合 `cat /sys/fs/cgroup/cpuacct/cpuacct.usage`。
追问三:没有 jstack 权限或者 root 都 dump 不出来怎么办?
几种办法:① `sudo -u <跑进程的用户> jstack
实战踩坑:正则表达式 ReDoS 吃满 CPU
某元宇宙社交应用的一个 Flink 作业,每 30 分钟左右会突然 CPU 100% 持续 20-30 秒然后恢复,Checkpoint 偶发超时。排查思路:既然是间歇性的,top -b 持续采样+jstack 定时 dump,在 CPU 高的瞬间抓堆栈。
# 写了个监控脚本,CPU 超过 90% 就自动 jstack
while true; do
CPU=$(ps -p 23451 -o %cpu | tail -1 | tr -d ' ')
if [ $(echo "$CPU > 90" | bc) -eq 1 ]; then
ts=$(date +%s)
jstack 23451 > /tmp/jstack_$ts.log
echo "[$ts] CPU=$CPU, dumped" >> /tmp/monitor.log
fi
sleep 2
done
抓到异常时刻的 jstack 后,发现几个 worker 线程全部卡在 `java.util.regex.Pattern$Loop.match`,堆栈里的业务代码是 UserAgent 解析那一段正则。翻代码发现写了个 `(.*)+.*` 这种带嵌套量词的正则,遇到特定长度的 UserAgent 会发生灾难性回溯(ReDoS),时间复杂度 O(2^n)。fix 是三条:① 正则预编译成 static Pattern;② 改成不回溯的写法;③ 加 UserAgent 长度限制(超过 500 字符直接返回默认值)。上线后 CPU 再没飙过。
总结
CPU 100% 排查的关键是背熟 `top -H -p → printf %x → jstack` 这条链路,然后能说出四种典型原因(计算密集/死循环/GC/锁争用)怎么区分。面试时主动提 `pidstat -u -t` 和 `async-profiler` 会让面试官眼睛一亮——这些工具不是书上有的,是真的踩过坑的人才用的。
下一篇聊一个让很多人翻车的内存题:free -h 里的 available 和 free 差了 20G,Linux 内存到底怎么看?OOM killer 又是怎么选进程的?
你们排查 CPU 高用什么工具?评论区聊聊有没有自动化的脚本~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长