
Java 服务 CPU 飙升至 100%?四步定位法,从「进程」追到「代码行」
Java 服务 CPU 打满接口卡死?别急着重启。四步法:top 定位进程 → top -H 定位线程 → printf 转十六进制 → jstack 抓堆栈,从堆栈直接看到具体代码行。含完整命令与死循环实战案例。
目录
Java 服务 CPU 飙升至 100%?四步定位法,从「进程」追到「代码行」
线上 Java 服务 CPU 突然打满、接口卡死,先别急着重启。重启只会让你丢失现场,真正的凶手还藏在那一行代码里。这篇文章带你把「进程 → 线程 → 堆栈 → 代码行」的排查链路完整走一遍,含完整命令、实战案例和进阶工具。
一、问题背景:你遇到的是不是这种场景
某天下午,告警群里突然炸了:
- CPU 使用率冲到 100%,甚至多个核全部打满;
- 接口响应从毫秒级变成「超时」,页面转圈,接口假死;
- 服务没崩、进程还在,但业务就是「卡住」了。
这时很多人的第一反应是直接重启服务。但这个动作其实是最不推荐的——因为重启会立刻清空线程堆栈和内存快照,你失去的正是定位问题最关键的证据。
正确的姿势是:先保留现场,再顺着「进程 → 线程 → 堆栈 → 代码行」逐层下钻,最快几分钟就能把肇事代码找出来。
二、排查前的准备:先「留现场」再动手
在开始定位之前,建议先把现场证据固化下来,避免排查过程中现场发生变化:
bash
# 1. 记录当前所有 Java 进程,确认目标进程还在
ps -ef | grep java
# 2. 抓一次线程堆栈,留作对比(排查开始前抓一次)
jstack <PID> > /tmp/jstack_before.txt
# 3. 如果条件允许,保留内存快照(用于后续分析 OOM / 对象问题)
jmap -dump:format=b,file=/tmp/heap.hprof <PID>
提示:
jstack抓堆栈本身也会短暂占用资源,但通常影响很小,可以在 CPU 高峰时连续抓 2~3 次。如果多次抓到的堆栈都指向同一个线程、同一个方法,基本可以实锤问题点。
一切就绪后,开始正式的四步排查。
三、四步排查法:从进程追到代码行
整个排查链路可以用一张流程图概括:
第一步:定位高 CPU 的进程(top)
打开终端执行 top,默认按 CPU 占用从高到低排列(如果没有,按键盘 P 键按 CPU 排序,q 退出)。
top - 14:22:31 up 3 days, 5:14, 2 users, load average: 4.83, 3.21, 2.05
Tasks: 235 total, 2 running, 233 sleeping
%Cpu(s): 98.7 us, 0.6 sy, 0.7 ni, 0.0 id, ...
KiB Mem : 16264860 total, 8056432 free, ...
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
19872 root 20 0 4778708 1.392g 13676 R 180.0 8.6 26:31.22 java
观察要点:
- %CPU 列:挑出数值最高(往往远超 100%,说明占用了多个核)的
java进程; - PID:记下它的进程号,本例为
19872; - TIME+:CPU 累计消耗时间,如果非常大,说明这个进程已经持续高占用很久了。
第二步:定位高 CPU 的线程(top -H -p)
拿到进程 PID 后,用 top -H -p <PID> 查看该进程内的所有线程及其 CPU 占用:
bash
top -H -p 19872
输出会列出线程级的 CPU 占用,同样按 P 排序,找到 %CPU 最高的那一行,记下它的 TID(线程号):
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
19872 root 20 0 4778708 1.392g 13676 R 89.5 8.6 26:12.01 java
19883 root 20 0 4778708 1.392g 13676 R 42.3 8.6 25:58.33 java
19876 root 20 0 4778708 1.392g 13676 R 12.1 8.6 3:05.12 java
本例高占用线程的 TID 为 19883。
第三步:把 TID 从十进制转成十六进制(printf)
这里是最容易踩坑的一步:top 显示的 TID 是十进制,而 jstack 输出的线程标识 nid 是十六进制。两者不换算,你是对不上号的。
用 printf 一行搞定:
bash
printf "%x\n" 19883
输出结果为十六进制,例如:
4dbb
记住这个值,它就是后面要在 jstack 里找的 nid。
也可以一行连写:
printf "%x\n" 19883,注意小写%x输出小写十六进制(jstack里的 nid 通常是小写,务必一致)。
第四步:抓取线程堆栈,定位到代码行(jstack)
抓取整个进程的线程堆栈:
bash
jstack 19872 > /tmp/thread_dump.txt
然后在堆栈文件中定位刚才那个十六进制的 nid:
bash
grep -A 30 "0x4dbb" /tmp/thread_dump.txt
-A 30 表示把匹配行及之后 30 行一起打印出来,正好覆盖一个线程的完整调用栈。重点看状态为 RUNNABLE 的线程——它说明该线程正在活跃执行 CPU 密集逻辑,通常就是问题的元凶。
"http-nio-8080-exec-1" #25 daemon prio=5 os_prio=0 tid=0x00007f2f7c04e800 nid=0x4dbb runnable [0x00007f2f52ffe000]
java.lang.Thread.State: RUNNABLE
at com.demo.OrderService.calculateTotal(OrderService.java:88)
at com.demo.OrderService.handleOrder(OrderService.java:45)
at com.demo.controller.OrderController.submit(OrderController.java:30)
...
看到 OrderService.calculateTotal(OrderService.java:88) 了吗?堆栈直接指向了具体的类和方法行号。到这里,排查链路就打通了——CPU 飙升的原因,就在这一行里。
四、实战案例:一个典型的死循环
下面用一个真实会发生的场景把整个流程串起来。
假设我们有一段业务代码:
java
public BigDecimal calculateTotal(List<Item> items) {
BigDecimal total = BigDecimal.ZERO;
int i = 0;
while (items.size() > i) { // 死循环:i 从未递增
total = total.add(items.get(i).getPrice());
// 忘写 i++; 这里 i 永远等于 0
}
return total;
}
while 循环里漏写了 i++,导致循环永不退出、疯狂执行 items.get(0)。正常情况下订单量不大、CPU 却居高不下,很可能就是这种逻辑缺陷。
按四步法排查:
top→ 找到 Java 进程19872,CPU 180%;top -H -p 19872→ 线程19883占用 89.5%;printf "%x\n" 19883→4dbb;jstack 19872 | grep -A 30 "0x4dbb"→ 堆栈停在OrderService.calculateTotal第 88 行的while上,状态RUNNABLE。
排查结论瞬间清晰:calculateTotal 第 88 行存在死循环,修复只需要补上 i++ 或改用增强 for 循环。整个定位过程 5 分钟以内。
五、常见原因速查表
不是所有 CPU 飙升都来自死循环。根据堆栈和现场特征,常见原因可快速归类:
| 现象特征 | 可能原因 | 进一步验证手段 |
|---|---|---|
堆栈指向某个 while / for 循环且状态 RUNNABLE |
死循环、空转自旋 | 检查循环退出条件、索引自增 |
| 多个线程 CPU 都高,堆栈指向集合/字符串操作 | 频繁正则回溯、大字符串处理 | 检查正则、substring、序列化 |
CPU 高但堆栈大量 GC / VM Thread |
Full GC 频繁 | 用 jstat -gcutil 观察 GC 频率 |
堆栈指向锁相关类(park / synchronized) |
锁竞争、自旋锁 | 结合 jstack 查看 BLOCKED 线程 |
| 业务量不大但 CPU 持续高 | 逻辑缺陷 / 空转任务 | 对比多次堆栈是否稳定同点 |
进阶判断:如果
top里是多个线程一起高,优先怀疑 GC 问题或并发空转;如果是单个线程独高,优先怀疑死循环/热点逻辑。
六、进阶排查工具(当基础四步不够用时)
四步法能解决 80% 的「单点逻辑」问题,但遇到更复杂的情况,可以上更多工具:
| 工具 | 作用 | 常用命令 |
|---|---|---|
jstat |
观察 GC 频率与内存分代 | jstat -gcutil 19872 1000 |
jmap |
导出堆快照,分析对象 | jmap -dump:format=b,file=heap.hprof 19872 |
jstack |
线程堆栈(多抓几次对比) | jstack 19872 > dump.txt |
arthas |
在线诊断,实时看热点方法 | ./as.sh 后 thread -n 3、profiler |
其中 Arthas 的 thread -n 3 可以一键列出 CPU 占用最高的前 3 个线程并抓取堆栈,非常省事,是四步法的强力替代方案。
七、总结与预防
一句话记住这套方法论:
top找进程 →top -H -p找线程 →printf "%x"转十六进制 →jstack抓堆栈 → 匹配 nid 定位到代码行。
几个关键要点:
- 别急着重启,先
jstack保留现场; top的十进制 TID 必须转成十六进制才能和jstack的nid对上;- 优先看 RUNNABLE 状态的线程,它直接指向执行中的热点代码;
- 多次抓堆栈对比,落在同一个点上基本可以实锤。
日常预防建议:
- 代码评审时重点检查循环退出条件、资源释放、正则表达式;
- 引入 Arthas 或 APM 工具,线上问题秒级定位;
- 对 CPU、线程数、GC 频率做好监控告警,把问题拦截在爆发前。
如果这篇文章对你有帮助,欢迎点赞、收藏、转发。排查线上问题是每个 Java 工程师的必修课,把这条链路刻进肌肉记忆,下次 CPU 打满时就能从容应对。