Java 服务 CPU 飙升至 100%?四步定位法,从「进程」追到「代码行」
🖼️ 运维日常· 2026-10-01 00:53 · 7

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 次。如果多次抓到的堆栈都指向同一个线程、同一个方法,基本可以实锤问题点。

一切就绪后,开始正式的四步排查。


三、四步排查法:从进程追到代码行

整个排查链路可以用一张流程图概括:

flowchart TD A["① top 找到 CPU 最高的 Java 进程"] -->|记录 PID| B["② top -H -p PID 找到 CPU 最高的线程"] B -->|记录 TID(十进制)| C["③ printf %x 把 TID 转成十六进制"] C -->|得到 nid 十六进制| D["④ jstack PID 抓堆栈"] D --> E["grep 查找对应 nid,定位 RUNNABLE 线程的代码行"]

第一步:定位高 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 却居高不下,很可能就是这种逻辑缺陷。

按四步法排查:

  1. top → 找到 Java 进程 19872,CPU 180%;
  2. top -H -p 19872 → 线程 19883 占用 89.5%;
  3. printf "%x\n" 19883 → 4dbb;
  4. 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 定位到代码行。

几个关键要点:

  1. 别急着重启,先 jstack 保留现场;
  2. top 的十进制 TID 必须转成十六进制才能和 jstack 的 nid 对上;
  3. 优先看 RUNNABLE 状态的线程,它直接指向执行中的热点代码;
  4. 多次抓堆栈对比,落在同一个点上基本可以实锤。

日常预防建议:

  • 代码评审时重点检查循环退出条件、资源释放、正则表达式;
  • 引入 Arthas 或 APM 工具,线上问题秒级定位;
  • 对 CPU、线程数、GC 频率做好监控告警,把问题拦截在爆发前。

如果这篇文章对你有帮助,欢迎点赞、收藏、转发。排查线上问题是每个 Java 工程师的必修课,把这条链路刻进肌肉记忆,下次 CPU 打满时就能从容应对。