跳到主要内容
约 8 分钟3 张卡

CPU 使用率

CPU 使用率表示进程消耗处理器时间的程度。本文先确认实例是否受到 CPU 限制,再从高 CPU 线程定位到调用路径。随后区分业务计算、GC、JIT 编译和本地代码的开销,最后说明如何验证修复。

1. 面试核心​

先记住这几条,再读正文
  • 线程标识对应。 同一 PID 命名空间内,Linux 平台线程的十进制 TID 转为十六进制后,对应线程转储中的 nid。卡 · CPU 饱和程度
  • CPU 采样。 CPU profile 通过一段时间内的采样占比,估计各调用路径消耗 CPU 的比例。卡 · 多份线程栈
  • 容器配额。 cgroup v2 的 cpu.max 规定每个周期可用的 CPU 时间,容器用完配额后会被节流。卡 · 容器配额与节流
  • 状态与实际消耗。 Java RUNNABLE 状态包含实际计算和部分 native I/O,判断 CPU 消耗还需要线程 CPU 时间。

2. CPU 饱和程度​

排查的第一步是确认 CPU 是否已经限制了服务处理能力。先对齐故障时间,再检查以下范围与资源指标:

  • 确认问题发生在单个实例,还是整个集群。
  • 比较 CPU 升高前后的吞吐和延迟,观察处理能力是否已经受限。
  • 对照流量、发布和定时任务的时间,寻找计算量变化的来源。
  • 区分 user、system 和 iowait。user 是用户态执行时间,system 是内核态执行时间,iowait 是 CPU 空闲且存在待完成 I/O 的时间。
  • 在容器中比较 CPU 使用量与配额,并检查 throttling,也就是配额用尽后的节流。

按单核计数时,100% 表示占用一个逻辑核。对于只有 1 CPU 配额的容器,这已经用完可用带宽;对于多核主机,则仍需结合核数和运行队列判断。

2.1 容器配额与节流​

容器会受到自己的 CPU 配额限制,即使主机还有空闲核也会被节流。Linux 用 cgroup 对进程分组并限制资源,cgroup v2 的 cpu.max 依次记录配额与周期。两个值都以微秒计:

200000 100000

这表示每 100ms 最多使用 200ms CPU 时间,对应平均 2 个 CPU 的带宽。多个线程并行运行时,可以在周期前段就用完配额。剩余时间要等待下个周期,因此请求会出现额外延迟。

在目标 cgroup 目录读取 cpu.stat,比较故障时间内两个计数的增量。nr_throttled 表示发生节流的次数,throttled_usec 记录节流时间。把增量与请求长尾对齐,可以判断配额是否影响了延迟。

配额不足时,应减少每个请求的计算量,或评估扩容。此时增加业务线程会让更多线程争用同一份配额,加剧等待。

3. 高 CPU 线程​

确认进程 CPU 高后,需要找出实际消耗 CPU 的线程。Linux 平台线程可以用以下命令连续观察:

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

记录持续高 CPU 的线程 ID(TID),避免只根据一次瞬时排序作判断。再把十进制 TID 转为十六进制,并采集线程转储:

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

在转储中查找对应的 nid=0x...,就能看到该线程的 Java 调用栈。TID 与转储需要来自同一 PID 命名空间,也就是使用同一套进程和线程编号。采集时间还要相近,避免线程退出或 ID 被复用后匹配错误。

Java 21 的虚拟线程由载体线程承载执行,并会在不同载体之间切换。因此,一个 OS TID 无法固定对应一个虚拟线程。此时要结合支持虚拟线程的 JFR 与分析工具,继续定位具体任务。

4. 多份线程栈​

单份栈只能显示线程在采集瞬间执行到哪里。间隔 3—10 秒采集数份,可以继续判断它是长期计算,还是偶然经过某个方法。重点观察以下线程行为:

  • 同一高 CPU 线程是否反复停在业务循环或正则匹配中。
  • 调用栈是否持续推进,但一直集中在同一算法或调用树。
  • 热点是否属于 GC Worker、CompilerThread 或 VM Thread。它们分别承担垃圾回收、JIT 编译或 JVM 内部工作。
  • 是否有大量可运行线程,伴随频繁的上下文切换。

相同栈帧反复出现,可以作为死循环或重复计算的线索。栈不断变化也可能持续消耗 CPU,因此还需要用一段时间的采样统计占比。分析时将业务、GC 和编译线程分开,并对照 OS 线程 CPU 时间。

5. CPU profile​

CPU profile 在一段时间内反复采样正在执行的调用栈,用来估计 CPU 时间花在哪里。线程栈没有明显循环时,可以用 async-profiler 采样 30 秒:

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

火焰图按调用关系展示样本,宽度表示采样占比。叶子是采样时的执行位置,父节点则包含子调用的成本。沿着宽的调用路径向上查找,可以把热点对应到请求、消息或定时任务入口。

常见的CPU 热点包括:

  • 序列化、压缩、加密和大集合排序带来的计算。
  • 无终止循环或指数级算法造成的重复工作。
  • 正则表达式反复尝试匹配路径,产生灾难性回溯。
  • 大量异常构造与堆栈填充。
  • 竞争自旋与高频 CAS 重试。
  • JNI 本地调用或内核系统调用。

CPU profile 的宽度不表示单次调用耗时。一个很短但被调用数百万次的方法,也能占据很宽的区域。

6. GC 与分配​

GC 线程消耗高,说明处理器时间花在回收上,还要继续查明回收工作为何增加。先检查分配率、存活对象量和堆配置,再用统计与 JFR 关联变化:

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

分配率表示单位时间创建对象的总量。分配峰值会更快消耗可用空间,增加回收频率。live set 是回收后仍存活的对象总量,无界保留会让它持续增长,增加复制、标记和空间压力。

对照 GC 日志时,要检查回收频率、GC CPU、回收后堆占用与分配停滞。若压力来自大量临时对象或无界保留,应优先修复这些来源,再评估 GC 线程配置。

各阶段的诊断方法见GC 停顿分析。

7. JIT、类加载与 native 时间​

CPU 还会消耗在 JVM 和本地代码中。发布后需要加载类,并由 JIT 把热点字节码编译成本地机器码,所以预热期间 CPU 会升高。可以对照 JFR 的 Compilation、Class Load 事件,以及 jcmd <pid> Compiler.queue 的编译队列。

system CPU 高时,需要继续检查内核与本地开销:

  • 线程创建和上下文切换是否频繁。
  • 网络或文件系统调用是否密集。
  • 是否存在缺页和内存压力。缺页指访问的页面尚未建立映射或尚未驻留,需要系统补充处理。
  • native 库或诊断 agent 是否消耗了大量 CPU。

Java 栈中的一个 native 方法可以包含大量本地工作。使用 async-profiler 的 native/kernel 栈,或系统 perf,可以继续展开调用路径。能否采集和解析这些栈,取决于平台权限与可用符号。

8. 修复验证​

修复需要证明同样的业务负载消耗了更少资源,且服务质量保持稳定。因此,前后应使用相近的流量和请求分布,比较以下CPU 成本与服务质量指标:

  • 每请求 CPU 或每秒累计 CPU 时间是否下降。
  • 吞吐、P99、超时率和错误率是否改善。P99 是 99% 请求都不超过的响应时间。
  • 分配率和 GC CPU 是否变化。
  • 线程数、上下文切换和 throttling 是否下降。
  • 热点调用树占比是否符合预期。

如果通过缓存减少计算,还需要验证内存容量和数据一致性。CPU 下降却显著增加延迟或内存占用时,应继续评估这次修改的代价。

9. 常见问题​

9.1 Load Average 与 CPU 使用率​

Linux Load Average 统计可运行任务和不可中断等待任务,所以 I/O 等待也能让它升高。判断 CPU 是否饱和,需要结合 CPU 使用率、运行队列、iowait 和具体线程。

9.2 线程数量与 CPU 消耗​

9.3 RUNNABLE 与实际计算​

10 · 面试题卡片​

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

出现公司:淘天、美团

考察重点:从容器和进程定位到线程,再用 TID、线程栈与 CPU profile 找到热点调用路径。

相关内容:CPU 饱和程度至JIT、类加载与 native 时间。

参考回答

先确认 CPU 是否饱和,再定位持续高 CPU 的线程,最后用调用栈和采样把它对应到代码。

具体可以沿着进程、线程和调用路径排查:

  1. 确认受影响实例、容器配额,以及 user、system 和 throttling 的变化。
  2. 用 top -H -p <pid> 或 pidstat -t 找持续高 CPU 的 TID。把它转为十六进制,在同一命名空间的转储中匹配 nid。
  3. 间隔采集多份 jcmd <pid> Thread.print -l,初步区分业务、GC、JIT 和 native 工作。
  4. 用短时 CPU profile 统计热点调用树,再与 GC、编译事件和发布时间线关联。

修复后,在相近负载下比较每请求 CPU、P99、分配率和节流情况,确认收益能够持续。

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

出现公司:阿里巴巴

考察重点:解释线程快照为何难以反映 CPU 占比,并用采样区分业务、GC、JIT 和 native 热点。

相关内容:多份线程栈至JIT、类加载与 native 时间。

参考回答

下一步采集 CPU profile,找出占比最高的调用树。

线程可以一直在多个方法间推进,一份快照难以反映累计计算量。因此,需要把采样结果与 GC、JIT 和类加载事件关联,区分业务工作和 JVM 内部工作。

若 system CPU 高,再检查上下文切换、系统调用和缺页。若热点集中在 native 方法中,则展开 native/kernel 栈,继续定位实际执行的函数。

10.3 主机 CPU 仍有空闲,容器为何出现节流和长尾?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释配额如何被并发线程提前用完,并用节流增量验证请求长尾的原因。

相关内容:容器配额与节流。

参考回答

容器会因自己的 CPU 配额用尽而被节流,与主机是否还有空闲核是两件事。

cpu.max 为 200000 100000 时,每 100ms 可用 200ms CPU 时间,平均带宽为 2 个 CPU。

多个线程可以在周期前段用完这份配额,随后等待下一周期,导致请求长尾。可以将 cpu.stat 中 nr_throttled 和 throttled_usec 的增量与延迟对齐。确认关联后,再评估减少每请求计算量或增加配额。

11. 参考资料​