最后更新:2026-10-29
适用场景:Linux、HotSpot JDK 17/21、Java 服务 CPU 持续升高或间歇性打满

告警里只有一条“java CPU 100%”,还没法判断该改哪段代码。先把三个问题串起来:是哪个进程在忙、进程里哪个线程在忙、这个线程当时在执行什么。证据留住之后,再决定要不要限流、回滚或重启。

下面的命令使用 Linux Bash 和 procps 的 top,不是 Windows PowerShell。PID、TID、目录都要换成实际值;示意调用栈也不是某次线上事故的原始记录。

先确认这个 100% 是怎么算的

先看监控的统计口径。top 在常见的 Irix 模式下,进程的 100% 大致代表用满一个逻辑 CPU,多线程进程可以超过 100%;切到 Solaris 模式后,数值会按 CPU 数量折算。八核机器上一条 java 进程显示 100%,不等于整台机器的八个核都满了。top 手册解释了两种模式。

容器面板还可能按容器配额计算百分比。先核对实例 CPU 限额、是否被节流,以及告警对应的具体实例,再和宿主机数字比较。不要把两个不同分母的“100%”放在一起下结论。

同时看 CPU 上升时的请求量、接口耗时、错误率和最近一次发布。正常批处理把 CPU 用起来,与流量没变但接口突然变慢,处理优先级并不相同。若机器 CPU 很高而 Java 进程不高,应先查其他进程和主机负载。

从进程找到持续忙碌的线程

先确认进程。能看到同一环境里的 JVM 时,可以用 jcmd -l 对照启动信息;容器里的 JVM 可能不会出现在宿主机的这份列表中,需结合平台进程信息确认。

下面假设已经确认目标进程为 24680。先准备一个只允许当前用户访问的取证目录,再观察线程:

java_pid=24680
umask 077
case_dir="${TMPDIR:-/tmp}/java-cpu-${java_pid}-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$case_dir"
cd "$case_dir" || exit 1

top -H -p "$java_pid"

-H 展示线程。进入 top 后按大写 P,按 CPU 使用率排序,连续看几次刷新。线程模式下列表的 PID 列通常就是操作系统线程 ID(TID),别拿整个 Java 进程的 PID 去代替它。

如果要保留几次快照,可以退出交互界面后执行:

top -H -b -d 2 -n 3 -p "$java_pid" > cpu-threads.txt

这会保留三次显示,间隔两秒。重点比较后续刷新里是否总是同一批线程排在前面;一次瞬时冲到最前面的线程,还不能直接判成问题线程。

这些观察和后面的 JVM 取栈必须对准同一个实例、同一个 PID 命名空间。 不要拿宿主机看到的 TID,直接到容器里找相同数字。容器内缺少诊断工具时,走现有平台的诊断流程,不要先在生产镜像里临时装一大批工具。

用 TID 对到 Java 调用栈

使用有权限访问目标进程、与目标 JDK 兼容的 jcmd。官方要求它与目标 JVM 位于同一机器,并使用相同的有效用户与组身份;具体支持的命令可用 jcmd <pid> help 检查。jcmd 文档也注明了各命令的影响等级。

确认权限后,在刚才的取证目录中保存三份调用栈:

jcmd "$java_pid" Thread.print -l > thread-1.txt
sleep 3
jcmd "$java_pid" Thread.print -l > thread-2.txt
sleep 3
jcmd "$java_pid" Thread.print -l > thread-3.txt

Thread.print 的影响随线程数量变化,不是完全没有成本。先做少量采样,不要用高频循环一直取栈;如果命令报错,先处理权限、工具或进程不匹配的问题,不能把只有错误信息的文件当成有效调用栈。

假设持续高 CPU 的线程 TID 是 24713,把十进制转成十六进制:

os_tid=24713
tid_hex=$(printf '0x%x' "$os_tid")
printf '%s\n' "$tid_hex"
# 此例输出 0x6089

grep -n -F -A 25 "nid=$tid_hex " thread-1.txt thread-2.txt thread-3.txt

在传统 HotSpot 平台线程转储里,nid 对应操作系统线程 ID,通常按十六进制显示。不要把线程名、Java 线程编号或 tid=... 后的地址当成这个 TID。

下面只是用于阅读的示意片段:

"http-nio-8080-exec-7" ... nid=0x6089 runnable ...
   java.lang.Thread.State: RUNNABLE
        at com.example.report.ReportCalculator.calculate(ReportCalculator.java:87)
        at com.example.report.ReportService.export(ReportService.java:42)

如果同一个 TID 在 CPU 快照里持续靠前,多份调用栈又反复落在 calculate 的同一段逻辑,才有理由沿着这段计算继续查:循环是否能退出、输入规模是否异常、是否反复扫描同一份集合,以及调用次数有没有突然增加。

反过来,只看到 RUNNABLE,不能证明这个线程正在大量消耗 CPU。先有操作系统的 CPU 证据,再用同一时间段的栈解释它。栈里很多线程在 WAITING、TIMED_WAITING 或等锁,也不等于它们就是 CPU 热点。

热点一闪而过时,补一段 JFR

手工取三次栈适合持续热点。每次去看都恢复正常,或者热点在多个线程之间切换时,可以用 Java Flight Recorder(JFR)留下一段短时间采样。

先检查目标 JVM 支持的参数:

jcmd "$java_pid" help JFR.start

确认后,在允许采样的时间窗口录制 60 秒。下面的 /var/tmp 是示意目录,必须是目标 JVM 所在环境中存在、且 JVM 运行用户有权写入的路径:

jcmd "$java_pid" JFR.start name=cpu-check settings=profile duration=60s filename=/var/tmp/java-cpu-%p-%t.jfr

这里的 %p、%t 由 JVM 展开为进程号和时间戳。查看命令返回的实际文件路径,等录制完成后再取文件。重复执行前先用 jcmd "$java_pid" JFR.check 检查已有录制,不要不断叠加采样任务。

profile 比默认录制收集更多信息,也有额外开销,适合短时间定位;采样前检查磁盘空间,录制期间留意服务延迟。用 JDK Mission Control 打开文件,对齐 CPU 升高的时间段,查看热点方法、调用树以及 GC 活动。

热点方法的采样占比是定位线索,不是某方法精确占用了多少 CPU 的证明。采样少、时间段没选对,结论都容易偏。Oracle 的 JFR 排障说明介绍了这些视图和采样的限制。

如果应用使用 JDK 21 虚拟线程,前面的 TID → nid 对照主要适用于平台线程。虚拟线程由载体线程调度,不能把 top 里的 TID 当作虚拟线程 ID;需结合 JFR 和支持虚拟线程的 Thread.dump_to_file 等工具继续分析,不能只依赖传统平台线程转储。虚拟线程诊断文档说明了这种区别。

找到热点以后,按证据处理

已有证据下一步怎么查
一个业务线程持续高 CPU,栈反复落在同一循环核对退出条件、输入大小、算法复杂度和重复计算
多个业务线程同时高 CPU,请求量也明显上涨对照吞吐与实例容量,再决定限流、扩容或优化热点
GC 活动与 CPU 高峰重合结合 GC 日志或 JFR 查对象分配、回收频率,别直接把堆调大
异常、重试或日志处理反复出现找重试触发原因和退避设置,检查是否形成失败重试循环
大量请求在等数据库连接,但这些线程 CPU 不高继续查连接池和数据库耗时,不能只凭“线程很多”判定 CPU 根因

最后一种情况可以接着看数据库连接池耗尽怎么排查。如果接口已经从网关返回 504,排查时也要把 CPU 高峰和失败请求的时间对齐,见接口返回 502/504 怎么定位。

业务已明显受影响时,限流、回滚或重启可以作为恢复手段,按现有预案执行;条件允许就先保留上面的少量证据。恢复后,用相近的负载复测 CPU、接口耗时、错误率和热点调用。CPU 降了但吞吐也掉了一半,不能算优化已经验证成功。

取证文件可能包含业务类名、调用路径和环境信息,按内部故障资料保管,不要原样贴到公开工单。修复记录里保留“当时谁在用 CPU、依据是什么、改了哪里、如何复测”,下次再遇到就不用从一张告警截图重新猜起。

参考资料

top:线程模式与 CPU 百分比
JDK 21:jcmd 命令与参数
JDK 21:使用 JFR 排查性能问题
JDK 21:虚拟线程与诊断工具

更新记录

2026-10-29:创建文章,整理进程、热线程、调用栈与 JFR 的定位步骤,并补充容器与虚拟线程的适用边界。