返回首页

Java 应用 CPU 100% 排查实战 — 从告警到代码行的四步法

📅 创建于 2026-05-19 🔄 更新于 2026-05-19 📝 524 字

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

注意事项:

  • 应用无响应时加 -Fjstack -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-diveLinux 内存管理(GC 根因涉及内存)