跳到主要内容

CPU 升高的排查路径

Java 进程 CPU 升高时,先确认是业务线程、GC、JIT 还是 native/内核工作,再把高 CPU 的操作系统线程映射到 Java 栈。线程总数多、Load Average 高或请求变慢,都不能单独证明 CPU 已经成为瓶颈。

1. 确认范围与饱和程度

先回答:

  • 是单个实例还是整个集群。
  • 容器 CPU usage 是否接近 quota。
  • user、system、iowait、throttling 分别怎样。
  • 吞吐和延迟是否同时变化。
  • 发生前是否有流量、发布或定时任务。

在容器中,一个进程显示 100% 可能只占一个核,也可能已经用完 1 CPU 配额。使用率还要结合 throttled time 和机器核数解释。

2. 定位进程和高 CPU 线程

Linux 示例:

top -H -p <pid>
pidstat -t -p <pid> 1

记录持续高 CPU 的线程 ID(TID),不要只看一次瞬时排序。将十进制 TID 转为十六进制,以匹配传统线程转储中的 nid

printf '%x\n' <tid>
jcmd <pid> Thread.print -l > threads.txt

找到相同 nid=0x... 的线程和栈。不同 JDK 的线程转储格式会变化,虚拟线程也不等同于一个固定 OS 线程;CPU 映射主要针对实际承载执行的操作系统线程。

3. 多份线程栈区分持续热点

间隔 3—10 秒采集数份,观察同一线程:

  • 是否持续停在同一业务循环或正则回溯。
  • 栈是否推进,只是某个算法长期占用 CPU。
  • 是否为 GC Worker、CompilerThread 或 VM Thread。
  • 是否大量线程 RUNNABLE 且频繁切换。

持续同栈提示死循环或重计算;栈不断变化但始终集中在同一调用树,更适合用 CPU profile 统计。

4. 用 CPU profile 获得调用树占比

asprof -d 30 -f cpu.html <pid>

火焰图宽度代表采样占比,不代表单次调用耗时。先看最宽的叶子和调用路径,再映射到请求、消息或定时任务入口。

典型热点:

  • 序列化、压缩、加密和大集合排序。
  • 无终止循环或指数级算法。
  • 正则灾难性回溯。
  • 大量异常构造与堆栈填充。
  • 竞争自旋和高频 CAS 重试。
  • JNI 或内核系统调用。

5. 检查 GC 和分配

GC 线程占用高通常是结果,背后可能是分配率、live set 或堆配置:

jstat -gcutil <pid> 1000
jcmd <pid> JFR.start name=cpu settings=profile duration=60s filename=cpu.jfr

同时查看 GC 日志中的回收频率、GC CPU、回收后堆和分配停滞。减少对象分配或无界保留可能比调整 GC 线程数更有效。

6. 检查 JIT、类加载和 native 时间

发布后短时 CPU 高可能来自类加载与 JIT 预热。JFR 的 Compilation 和 Class Load 事件、Compiler.queue 可验证。

system CPU 高时检查:

  • 线程创建与上下文切换。
  • 网络/文件系统调用。
  • page fault 和内存压力。
  • native 库或 agent。

只看 Java 栈可能把 native 热点显示为一个宽泛的 native 方法。async-profiler 的 native/kernel 栈或系统 perf 能继续展开,前提是权限和符号可用。

7. 修复后按同一负载验证

至少比较:

  • 每请求 CPU 或每秒 CPU time。
  • 吞吐、P99 与超时率。
  • GC CPU 和分配率。
  • 线程数、上下文切换和 throttling。
  • 热点调用树占比。

降低 CPU 但显著增加延迟或内存,不是完整修复。若通过缓存换 CPU,还要验证缓存上限和一致性。

8. 常见问题

8.1 Load Average 高就是 CPU 高吗

不是。不同系统的 load 还可能包含等待不可中断 I/O 的任务。要看 CPU usage、run queue、iowait 和具体进程线程。

8.2 大量线程是 CPU 高的直接原因吗

不一定。大量 WAITING 线程可能几乎不消耗 CPU,但占栈内存;大量可运行线程会增加调度和切换。用状态和 CPU time 判断。

8.3 看到 RUNNABLE 就说明线程在计算吗

不一定。Java RUNNABLE 包含一些在 native I/O 中的状态。结合多份栈、线程 CPU 和 wall/CPU profile。

9. 面试题

9.1 Java 进程 CPU 飙高,怎样定位到具体代码

出现公司:淘天、美团

考察重点

  • 容器/进程/线程三级定位。
  • TID 与线程转储 nid 的映射。
  • 多快照和 CPU profile。

相关内容:第 1 节“确认范围与饱和程度”至第 6 节“检查 JIT、类加载和 native 时间”。

参考回答

先确认 CPU 配额、user/system/throttling 和受影响实例,再用 top -Hpidstat -t 找持续高 CPU 的 TID。把 TID 转成十六进制,在多份 jcmd Thread.print 中匹配 nid,初步确认业务、GC、JIT 或 native 线程。

随后用短时 CPU profile 获得调用树占比,并把结果与 GC、编译和发布时间线关联。修复后比较每请求 CPU、P99、分配率和 throttling,不能只看一个瞬时使用率。

9.2 CPU 高但线程栈没有明显死循环,下一步看什么

出现公司:阿里巴巴

考察重点

  • 栈快照不能给出时间占比。
  • CPU profiler、GC/JIT 和 native 栈。
  • system CPU 与上下文切换。

相关内容:第 3 节“多份线程栈区分持续热点”至第 6 节“检查 JIT、类加载和 native 时间”。

参考回答

线程可能在很多方法间推进,一份快照看不到占比。采集 CPU profile,确认最宽的调用树;同时检查 GC 线程、编译线程和类加载事件。如果 system CPU 或 native 方法占比高,再看上下文切换、系统调用、page fault 和带 native/kernel 栈的 profile。