前言

CPU 飙高是 Java 线上故障中非常典型的场景。面试官常问:CPU 100% 怎么排查?怎么定位到具体线程?怎么从线程栈找到代码?如果是 GC 导致的 CPU 高怎么办?

典型现象

  • 单台机器 CPU 长时间 90% 以上。
  • 接口响应变慢,甚至健康检查失败。
  • 负载均衡把流量打到其他机器后,其他机器也升高。
  • 日志量突然增加。
  • Full GC 或 Young GC 次数异常增多。

CPU 高不一定是死循环,也可能是频繁 GC、日志刷盘、序列化、大量正则匹配、加密计算、压缩解压、热点锁自旋等。

排查流程

flowchart TD
    A[CPU飙高] --> B[定位进程]
    B --> C[定位高CPU线程]
    C --> D[转换线程ID]
    D --> E[jstack查看线程栈]
    E --> F{线程在做什么}
    F --> G[业务死循环]
    F --> H[频繁GC]
    F --> I[锁竞争或自旋]
    F --> J[序列化/正则/加密]
    G --> K[止血和修复]
    H --> K
    I --> K
    J --> K

常用命令

定位 Java 进程:

1
2
jps -l
top

定位进程内高 CPU 线程:

1
top -Hp <pid>

把线程 ID 转成 16 进制:

1
printf "%x\n" <tid>

导出线程栈:

1
jstack -l <pid> > jstack.log

然后在 jstack.log 中搜索 nid=0x...,就能找到对应线程正在执行的 Java 代码。

线程栈怎么看

线程栈一般关注:

  • 线程名称。
  • 线程状态。
  • nid。
  • 栈顶方法。
  • 是否反复出现在同一段代码。
  • 是否有锁等待。

常见线程状态:

状态 含义 排查重点
RUNNABLE 正在运行或等待 CPU 死循环、计算密集、IO native 调用
BLOCKED 等待 synchronized 锁 锁竞争、锁粒度过大
WAITING 无限等待 队列消费、条件等待
TIMED_WAITING 限时等待 sleep、park、连接池等待

CPU 高时,最关键的是高 CPU 线程的栈顶方法。如果多次 jstack 都停在同一业务方法,基本可以锁定该方法。

常见根因

死循环

典型代码:

  • while 条件永远不退出。
  • 分页游标没有推进。
  • 重试没有最大次数。
  • 状态机异常回到原状态。
  • 递归缺少终止条件。

生产中常见的是数据触发死循环,比如某条异常订单导致流程一直补偿,或者某个导入文件导致解析循环无法结束。

正则或 JSON 解析过重

复杂正则遇到特殊输入可能出现灾难性回溯。大 JSON 反序列化也可能吃满 CPU。

排查特征:

  • 线程栈停在 PatternMatcher、JSON 解析库。
  • 某类请求参数触发。
  • CPU 高但内存和 DB 压力不明显。

日志打印过多

异常被循环打印,或者 DEBUG 日志在线上打开,会导致 CPU 和 IO 同时升高。

排查特征:

  • 日志文件增长很快。
  • 线程栈在日志框架、字符串拼接、异常堆栈输出。
  • 磁盘 IO 也升高。

GC 线程占用 CPU

如果高 CPU 线程是 GC 线程,就要转向内存排查。

需要检查:

  • GC 日志。
  • 堆使用率。
  • 对象分配速率。
  • 是否频繁 Full GC。
  • 是否存在内存泄漏。

锁竞争或自旋

锁竞争严重时,线程可能大量阻塞,也可能因为自旋消耗 CPU。

常见原因:

  • 锁粒度过大。
  • synchronized 包住远程调用或慢 SQL。
  • 热点账户、热点库存、热点配置被串行化。
  • CAS 重试过多。

生产止血

  • 摘除高 CPU 实例,保留现场后再重启。
  • 导出 jstack、GC 日志、应用日志。
  • 如果是新版本问题,快速回滚。
  • 如果是特定请求触发,网关临时限流或拦截。
  • 如果是日志风暴,临时调整日志级别。
  • 如果是补偿任务死循环,暂停任务。

注意:直接重启可能恢复服务,但会丢失现场。生产排查最好先抓线程栈和必要日志。

长期优化

  • 给循环、重试、分页任务加最大次数和保护条件。
  • 核心线程池增加监控。
  • 对 CPU 密集任务做隔离。
  • 对复杂正则做输入限制和压测。
  • 给批处理任务增加进度日志。
  • 接入线程栈自动采样工具。

高频面试题

CPU 100% 怎么定位到代码?

先用 top 找到 Java 进程,再用 top -Hp <pid> 找到高 CPU 线程,把线程 ID 转 16 进制,然后用 jstack 搜索对应 nid,查看线程栈顶业务方法。

jstack 只抓一次够吗?

不够。最好连续抓 3 到 5 次,间隔几秒。如果同一个线程多次停在同一代码位置,更能证明该代码在消耗 CPU。

CPU 高一定是死循环吗?

不是。也可能是频繁 GC、日志风暴、正则回溯、加密压缩、大量序列化、锁自旋或高并发正常消耗。

总结

CPU 飙高排查的关键路径是进程、线程、nid、jstack、代码位置。面试时要把命令链路讲清楚,也要说明如何保留现场和快速止血。