Skip to content
应用运行时异常排查 · 第 1 篇 / 共 4 篇
领域生产问题
专题应用运行时排障专题
当前序列应用运行时异常排查
阅读位置第 1 篇 / 共 4 篇当前专题第 1 个序列 / 共 3 个序列

生产问题:Java 服务 CPU 飙升时怎么排查

CPU 飙升是线上最典型、也最容易让人慌的一类问题。

真正排查时,最怕的不是不会命令,而是一上来就乱抓现场,最后把最关键的信息弄丢。

它和“接口变慢但 CPU 不高”是两类完全不同的问题。

CPU 飙升通常意味着:

  • 有线程在持续执行计算
  • 有代码在自旋、死循环或高频重试
  • 或者 GC 本身已经开始大量吃 CPU

先说结论

CPU 飙升时,最稳妥的顺序通常是:

  1. 先确认是不是 Java 进程导致
  2. 再确认是哪个线程在吃 CPU
  3. 再结合线程栈看它在干什么
  4. 如果怀疑内存或对象问题,再补抓 Heap Dump

更直接一点说,CPU 问题最重要的不是“先抓一堆文件”,而是先把最高 CPU 线程和对应线程栈对上。

因为大多数 CPU 故障,答案就在线程栈里。

一个典型故障现场

比如某个 Java 服务平时 CPU 在 20% 左右,某次发版后突然出现:

  • 单机 CPU 长时间 90%+
  • QPS 没明显上升
  • 接口开始超时
  • 线程池活跃数也很高
  • GC 次数看起来增加,但又不是每次都在 Full GC

这时候最容易犯的错是:

  • 一上来就抓 Heap Dump
  • 一上来就怀疑数据库
  • 一上来就直接重启

这些动作有时能缓解,但不一定能帮你保住真正现场。

一、先看进程

bash
jps -l -v

先确认:

  • 目标服务 PID
  • 启动参数

这一步除了确认 PID,还要顺手看:

  • 最近是否发版后 JVM 参数改过
  • 是否有多个 Java 进程同时存在
  • 问题是不是落在预期那个服务上

很多现场其实不是“主服务 CPU 高”,而是旁边某个消费者、任务进程、工具进程在占 CPU。

二、再看线程

如果要找具体是谁在吃 CPU,常见思路是:

  • 用系统命令看高 CPU 线程
  • 再把线程 ID 转换后到 jstack 里定位

CPU 问题的核心现场,通常在线程栈,而不是 Heap Dump。

真正更有价值的问题是:

  • 这个线程是在算什么
  • 是正常业务计算、死循环、自旋等待,还是 GC 线程
  • 是单个线程特别高,还是很多线程一起高

单线程高,常常更像:

  • 死循环
  • 自旋重试
  • 某段算法逻辑异常

多线程都高,常常更像:

  • 线程池大量执行热点任务
  • 大规模序列化/反序列化
  • 批量计算、批处理、索引构建
  • 或者 GC 正在大量抢 CPU

三、先分清是业务线程高,还是 GC 在吃 CPU

这一步很关键,因为处理方向完全不同。

1. 如果是业务线程高

更要看线程栈是否集中在:

  • 某个 while 循环
  • 某个自旋 CAS 逻辑
  • JSON/对象转换
  • 正则匹配
  • 排序、聚合、加解密、压缩

2. 如果是 GC 线程高

就要继续判断:

  • 是 Young GC 太频繁
  • 还是 Full GC 已经频繁
  • GC 后堆是否明显回落

这时候 CPU 问题和内存问题往往已经绑在一起了。

四、什么时候抓 Heap Dump

如果你看到:

  • CPU 高的同时伴随频繁 GC
  • 怀疑对象堆积或内存问题

这时可以考虑:

bash
jmap -heap <pid>
jmap -dump:format=b,file=heap.hprof <pid>

如果进程状态异常,也可能会遇到强制抓取场景。

但有一条经验很重要:

  • 纯 CPU 问题优先抓线程栈
  • 内存和 GC 明显异常时再补 Heap Dump

因为 Heap Dump 适合回答“对象为什么这么多”,不适合直接回答“哪个线程为什么把 CPU 打满了”。

五、线上最常见的根因画像

线上 CPU 飙升常见原因包括:

  • 死循环
  • 自旋重试过多
  • 锁竞争激烈
  • 线程池打满后业务反复重试
  • 频繁 Full GC

再展开一点,特别高频的还有:

1. 代码死循环或边界条件错误

最典型,也最容易在发版后立即出现。

2. 自旋和重试策略过激

例如某个失败重试没有 sleep,没有退避,错误时瞬间把 CPU 打穿。

3. 热点数据导致大量重复计算

比如:

  • 大对象序列化
  • 大 JSON 解析
  • 排序和聚合
  • 报表或导出计算

4. GC 压力过大

这类通常会伴随:

  • 分配速率高
  • 对象生命周期不合理
  • 本地缓存或集合堆积

5. 线程池把高开销任务同时放大

线程池本来是为了并发提速,但如果把 CPU 密集任务一口气放大到很多线程,也会把整机打满。

六、一个更实用的排查顺序

线上 CPU 飙升时,我通常按这个顺序看:

  1. 先确认是不是 Java 进程,以及哪台机器最明显。
  2. 看是单核拉满还是多核整体高。
  3. 找高 CPU 线程,并转到 jstack 定位线程栈。
  4. 判断是业务线程、自旋线程还是 GC 线程。
  5. 如果伴随 GC 异常,再补 GC 日志和 Heap Dump。
  6. 最后再结合最近发版、流量变化、任务执行情况定根因。

这个顺序的重点就是:

  • 先抓最接近真相的线程现场
  • 再决定是否需要更重的堆分析

七、一个实用提醒

如果问题明确是 CPU 飙升,不要一开始就把重心全放在 Heap Dump。

因为:

  • Heap Dump 更适合看内存对象

  • CPU 更需要看线程执行栈

  • 重启和大动作会直接破坏最重要的线程现场

一句话总结

CPU 飙升问题最关键的是先抓高 CPU 线程现场,再判断是否需要补看 GC、内存和对象。

线程栈通常比堆快照更接近答案。先搞清楚“是谁在烧 CPU”,问题往往就已经解决了一半。

延伸阅读相关文章优先当前专题,再补跨专题关联。
同一序列 · 顺着当前主线继续读Full GC 频繁时怎么排查适合把 CPU、Full GC、线程池和接口超时这类应用层异常放在一组里看。应用运行时排障专题 · 应用运行时异常排查同一序列 · 顺着当前主线继续读线程池打满时怎么排查适合把 CPU、Full GC、线程池和接口超时这类应用层异常放在一组里看。应用运行时排障专题 · 应用运行时异常排查同专题其他序列 · 共享标签:线上排障、案例排障接口超时排查为什么要区分 RT 和 queue wait适合把线程死锁、CPU 高、Full GC、接口超时和 OOM 现场保留放在一条运行时排障主线上看。应用运行时排障专题 · 应用运行时故障的现场与根因收敛同专题其他序列 · 共享标签:线上排障、案例排障线程池积压时怎么判断是慢任务还是流量放大适合把线程死锁、CPU 高、Full GC、接口超时和 OOM 现场保留放在一条运行时排障主线上看。应用运行时排障专题 · 应用运行时故障的现场与根因收敛跨专题关联 · 共享标签:线上排障、案例排障磁盘打满后为什么删除文件不一定立刻生效适合把 Redis 抖动、MySQL 连接打满、MQ 积压、ES 发黄、ClickHouse 合并堆积和磁盘打满放在同一条基础设施排障主线上看。数据与基础设施排障专题 · 数据与基础设施故障的分层排查跨专题关联 · 共享标签:线上排障、案例排障大 Header、buffer、timeout 问题怎么排查适合把 location 匹配、缓冲区和负载均衡放在一起看。容器与站点部署专题 · Nginx 进阶路由与代理治理
继续阅读应用运行时异常排查当前序列第 1 篇 / 共 4 篇当前专题第 1 个序列 / 共 3 个序列
往前看
上一序列业务稳定性与治理实践从第 1 篇开始:支付回调幂等到底该怎么落地
往后看
下一篇Full GC 频繁时怎么排查继续当前序列下一章下一序列应用运行时典型故障案例从第 1 篇开始:Redis 热 Key 突增时怎么止血和回查

把零散经验整理成可查、可复用、可持续更新的企业级知识门户