Java 应用 CPU 100% 排查实战
以面试场景切入,记录一次 order-service 生产环境 CPU 100% 的完整排查过程。 排查方法论参见 cpu-spike-troubleshooting-guide(通用 Linux 四层体系),三命令速查参见 cpu-spike-3-commands。
一、排查四步法(Java 特化版)
第 1 步:top 找进程
top
关注 %CPU 列。多个 Java 节点同时飙高 → 可能是流量/代码问题;单节点飙高 → 单机热点。
PID USER %CPU COMMAND
28947 appuser 398.7 java # 占满约 4 个 CPU 核心
第 2 步:top -Hp 找线程
top -Hp 28947
找到吃满 CPU 的线程:
http-nio-8080-exec-*→ Tomcat 工作线程,应用层问题GC task thread→ GC 线程高 → 可能是 Full GC 问题VM Thread→ JVM 自身操作
CPU 线程 PID 转十六进制(jstack 需要):
printf "%x\n" 29015 # 输出:7177
第 3 步:jstack 导堆栈
jstack 28947 > /tmp/thread_dump.txt
注意事项:
- 应用无响应时加
-F:jstack -F 28947 - 连续导出 3 次,间隔 5 秒,对比前后变化
- 容器环境需先进入:
docker exec -it container_id jstack PID
第 4 步:grep 定位代码行
grep "7177" -A 30 /tmp/thread_dump.txt
输出示例:
"http-nio-8080-exec-3" #78 daemon nid=0x7177 runnable
java.lang.Thread.State: RUNNABLE
at com.order.service.PriceCalculator.calculateDiscount(PriceCalculator.java:87)
从告警到代码行,最快 5 分钟搞定。
二、四大常见原因
原因一:死循环 — 最经典也最隐蔽
三种写法:
for (int i = 0; j < 10; i++)— 条件变量写错while (retryCount < 3) { ... }— 忘了递增,永远不退出if (n <= 0) return fibonacci(n)— 递归无终止条件
HashMap 死链(Java 7 及之前): 多线程并发扩容 HashMap 时,链表可能形成环形结构,get 操作无限遍历。jstack 中线程状态为 RUNNABLE,连续多次 dump 都卡在同一行代码。
原因二:正则表达式回溯 — 最容易被忽略的杀手
危险正则模式:
| 模式 | 示例 | 风险 |
|---|---|---|
| 嵌套量词 | (a+)+、(a*)*、(a+)* |
指数级回溯 |
| 交替重叠 | (a\|a)+ |
指数级回溯 |
| 量词+分组重叠 | (\w+\|\d+)+ |
指数级回溯 |
当输入不匹配时,回溯次数为 2 的 N 次方(N=字符串长度)。30 个 a 加一个不匹配字符 = 10 亿次。
修复方案:
- 使用占有量词:
([a-zA-Z0-9]++)+(匹配失败不回溯) - 限定输入长度:
if (email.length() > 100) return false - 避免嵌套量词:直接写
^[a-zA-Z0-9]+@[a-zA-Z0-9]+...
原因三:频繁 Full GC — 内存问题伪装成 CPU 问题
用 jstat 确认:
jstat -gcutil 28947 1000 10
关键指标:
- O(Old 区)使用率 99.87% → 老年代几乎满
- FGC(Full GC 次数)1 秒内从 234 涨到 237 → 每秒 3 次 Full GC ✓ 确诊
- FGCT(Full GC 总时间)56 秒 → 每次 200+ms
常见内存泄漏场景:
ThreadLocal没清理,线程池复用时对象堆积- 静态集合不断 put 但不 remove
- 大对象进老年代后无法回收(100MB byte[])
- 监听器/回调注册后没注销
# 快速诊断:堆直方图
jmap -histo:live 28947 | head -30
# 深度分析:导出 heap dump
jmap -dump:format=b,file=/tmp/heap.hprof 28947
原因四:序列化大对象 — 温水煮青蛙
// ❌ 一次查 50 万条,序列化时 CPU 喘不过气
@GetMapping("/orders/export")
public List<Order> exportOrders() {
return orderMapper.selectAll();
}
修复方案:
- 加分页限制
- 使用流式处理(边查边写,减少内存占用)
- 超大数据用异步任务 + 文件导出
三、真实案例:for 循环忘加 break
凌晨 2 点告警,order-service 两个节点 CPU 100%。
定位到 PriceCalculator.java:87:
// ❌ 问题代码
for (Promotion p : promotions) { // 加载了几千个促销策略
if (p.matches(order)) {
// 匹配成功应该 break,但 break 被注释掉了
// 还触发了递归,嵌套调用其他促销检查
}
}
根因: 匹配成功后没 break,遍历所有策略 + 递归调用,4 个线程全卡死。
修复: 加一行 break,RT 从 15 秒恢复到正常。
四、预防措施
Code Review 要点
- ✅ 检查所有循环,确认退出条件明确无误
- ✅ 检查所有递归,确认有明确的终止条件
- ✅ 检查所有正则,确认没有嵌套量词
- ✅ 检查所有对外接口,确认有超时设置
监控告警配置
# CPU 使用率超过 80% 持续 5 分钟
- alert: HighCpuUsage
expr: cpu_usage_percent > 80
for: 5m
labels:
severity: critical
# Full GC 频率超过 1 次/分钟
- alert: HighFullGC
expr: rate(jvm_gc_pause_count_total{type="full"}[1m]) > 1
labels:
severity: critical
关联页面
| 页面 | 关联点 |
|---|---|
| cpu-spike-troubleshooting-guide | 通用 Linux CPU 排查四层体系(整机→进程→线程→调用栈) |
| cpu-spike-3-commands | 三命令速查(top → strace → /proc/PID/fd/) |
| fullstack-performance-troubleshooting | 全栈性能排查(含 JVM GC 深度分析) |
| online-troubleshooting-checklist | 线上故障排查清单 |
| linux-memory-management-deep-dive | Linux 内存管理(GC 根因涉及内存) |