Java面试要点:CPU飙高、线程栈分析与死循环定位
前言
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 | jps -l |
定位进程内高 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。
排查特征:
- 线程栈停在
Pattern、Matcher、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、代码位置。面试时要把命令链路讲清楚,也要说明如何保留现场和快速止血。


