Java 进程 CPU 飙高排查全流程详解
一、先理解整体思路
当我们发现服务器 CPU 占用率异常高时,需要一步步缩小范围:
整台服务器
└── 找到哪个【进程】占用 CPU 高
└── 找到该进程中哪个【线程】占用 CPU 高
└── 查看该线程正在执行哪段【代码】
└── 定位问题根源(死锁 / 死循环)
这就是整个排查流程的核心逻辑:从粗到细,逐层定位。
二、第一步:找到占用 CPU 过高的进程
使用 top 命令
在 Linux 终端输入:
bash
top
你会看到一个实时刷新的界面,类似下面这样:
PID USER %CPU %MEM COMMAND
12345 root 99.8 2.1 java
6789 root 0.3 0.5 nginx
...
关键字段解释
| 字段 | 含义 |
|---|---|
PID |
进程ID,每个进程的唯一编号 |
%CPU |
该进程占用 CPU 的百分比 |
COMMAND |
进程对应的程序名称 |
此步骤的目标
找到 %CPU 数值异常高的那一行,记录下它的 PID。
例如上面例子中,java 进程的 PID 是 12345,CPU 占用 99.8%,这就是我们要重点排查的进程。
三、第二步:找到该进程中占用 CPU 过高的线程
为什么要找线程?
一个 Java 进程内部会同时运行很多线程 (比如处理请求的线程、垃圾回收线程、定时任务线程等)。进程的 CPU 高,一定是因为其内部某个或某几个线程在疯狂消耗 CPU,我们需要精确找到是哪个线程。
使用 top -H -p 进程PID 命令
bash
top -H -p 12345
参数解释:
-H:以线程为单位展示(默认 top 是以进程为单位)-p 12345:只看 PID 为12345的这个进程
执行后你会看到:
PID USER %CPU %MEM COMMAND
12350 root 99.5 2.1 java
12351 root 0.1 2.1 java
12352 root 0.0 2.1 java
...
这里每一行代表的是该 Java 进程内部的一个线程 ,PID 列此时显示的是线程ID。
此步骤的目标
找到 %CPU 异常高的那一行,记录下它的线程ID(十进制数字)。
例如上面例子中,线程ID 12350 的 CPU 占用高达 99.5%,这就是问题线程。
四、第三步:理解 jstack 命令
jstack 是什么?
jstack 是 Java 自带的一个工具,它能够打印出 Java 进程中所有线程当前的堆栈跟踪信息。
所谓堆栈跟踪信息,就是告诉你:某个线程此刻正在执行哪个类的哪个方法的第几行代码。
使用方式
bash
jstack 12345
执行后会输出大量内容,每个线程对应一段信息,格式大致如下:
"线程名称" #线程编号 ...
java.lang.Thread.State: RUNNABLE
at com.example.MyService.calculate(MyService.java:88)
at com.example.MyController.handle(MyController.java:45)
...
这段信息告诉我们:
- 这个线程当前处于什么状态(
RUNNABLE表示正在运行) - 它正在执行
MyService.java第 88 行的calculate方法 - 这个方法是被
MyController.java第 45 行调用的
五、第四步:解决线程ID进制不一致的问题
问题所在
这是整个流程中一个非常关键的细节:
| 工具 | 线程ID的表示方式 |
|---|---|
top -H -p |
十进制 (如 12350) |
jstack 输出 |
十六进制 (如 0x302e) |
两者展示的是同一个线程,但用了不同的进制,所以我们必须把十进制转换成十六进制,才能在 jstack 的输出中找到对应的线程。
为什么会有这种差异?
- Linux 系统工具(如
top)习惯使用十进制展示线程ID - Java 的
jstack工具在打印线程信息时,使用十六进制展示线程ID(这是 Java 内部的惯例)
两者本质上指的是同一个线程,只是数字的表达形式不同。
如何转换?使用 Linux 命令完成
bash
printf "%x\n" 12350
printf是 Linux 的格式化输出命令%x表示将数字以十六进制格式输出\n表示换行
执行结果:
302e
这样我们就知道,十进制的线程ID 12350,对应十六进制是 302e。
六、第五步:用 jstack 精确定位问题线程的代码
使用管道符 | 和 grep 过滤
jstack 输出的内容非常多(一个 Java 进程可能有几十上百个线程),我们不需要全部查看,只需要找到那个十六进制线程ID对应的那段信息。
bash
jstack 12345 | grep 302e
jstack 12345:打印出进程 12345 的所有线程堆栈信息|:管道符,把前面命令的输出,作为后面命令的输入grep 302e:在输出中搜索包含302e的行
执行结果可能如下:
"pool-1-thread-1" #25 prio=5 os_prio=0 tid=0x... nid=0x302e runnable
找到这一行后,查看它下面的堆栈信息:
"pool-1-thread-1" #25 prio=5 os_prio=0 nid=0x302e runnable
java.lang.Thread.State: RUNNABLE
at com.example.OrderService.computeDiscount(OrderService.java:156)
at com.example.OrderService.processOrder(OrderService.java:89)
at com.example.OrderController.submitOrder(OrderController.java:34)
这就清楚地告诉我们:问题线程正卡在 OrderService.java 的第 156 行。
七、第六步:分析问题根源
两种最常见的导致 CPU 飙高的原因
原因一:死循环
代码中存在一个永远不会结束的循环,线程一直在执行,不断消耗 CPU。
java
// 示例:死循环
while (true) {
// 某个条件判断逻辑写错了,导致永远无法退出循环
doSomething();
}
特征 :线程状态是 RUNNABLE(正在运行),且每次查看 jstack,该线程都停在同一段代码附近。
原因二:死锁
两个或多个线程互相等待对方释放资源,谁也无法继续执行。
线程A 持有锁1,等待锁2
线程B 持有锁2,等待锁1
=> 两个线程永久阻塞,形成死锁
特征 :线程状态是 BLOCKED(阻塞),jstack 甚至会在输出末尾直接提示 Found one Java-level deadlock。
如何确认
打开 jstack 定位到的源代码文件,找到对应行号,结合上下文逻辑,判断是否存在:
- 循环条件永远为真(死循环)
- 多个锁的获取顺序不一致(死锁)
找到问题代码后,修复逻辑,重新部署,CPU 即可恢复正常。
八、完整流程总结
第一步:top
↓ 找到 CPU 高的进程 PID(如 12345)
第二步:top -H -p 12345
↓ 找到 CPU 高的线程ID(十进制,如 12350)
第三步:printf "%x\n" 12350
↓ 转换为十六进制(如 302e)
第四步:jstack 12345 | grep 302e
↓ 找到问题线程正在执行的代码位置
第五步:打开源代码,定位到对应行
↓ 分析是死循环还是死锁
第六步:修复代码,解决问题
整个排查过程的本质,就是利用操作系统工具和 Java 工具的配合,将 CPU 异常这个宏观现象,一步步精确追溯到具体的一行代码,从而找到并解决问题。