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 消耗
处于 WAITING 的线程几乎不执行计算,但平台线程仍占用栈等资源。大量可运行线程则会增加调度和切换成本。因此,判断线程多是否造成高 CPU,要同时看线程状态和 CPU 时间。
9.3 RUNNABLE 与实际计算
Java RUNNABLE 包含部分 native I/O,所以看到这一状态还需要对照 OS 线程 CPU 和多份调用栈。若要分析请求的等待时间,可以使用 wall-clock profile。它采样经过的实际时间,包含线程等待和睡眠。
10 · 面试题卡片
10.1 Java 进程 CPU 飙高,怎样定位到具体代码
出现公司:淘天、美团
考察重点:从容器和进程定位到线程,再用 TID、线程栈与 CPU profile 找到热点调用路径。
相关内容:CPU 饱和程度至JIT、类加载与 native 时间。
参考回答
先确认 CPU 是否饱和,再定位持续高 CPU 的线程,最后用调用栈和采样把它对应到代码。
具体可以沿着进程、线程和调用路径排查:
- 确认受影响实例、容器配额,以及 user、system 和 throttling 的变化。
- 用
top -H -p <pid>或pidstat -t找持续高 CPU 的 TID。把它转为十六进制,在同一命名空间的转储中匹配nid。 - 间隔采集多份
jcmd <pid> Thread.print -l,初步区分业务、GC、JIT 和 native 工作。 - 用短时 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 的增量与延迟对齐。确认关联后,再评估减少每请求计算量或增加配额。