为什么你的 CPU 总是突然飙高?Java 生产环境 6 大排查误区

一、问题场景:CPU 飙高,但原因不明

服务没挂、内存也正常、数据库连接池也没满,但机器 CPU 突然飙到 90% 以上,请求延迟持续上升。

很多人第一反应是扩容、重启、加机器,但这些动作往往只是止血,不是定位问题。真正麻烦的是:CPU 高并不等于原因明显,很多时候线程栈、GC、频繁日志、错误重试、死循环、热点锁竞争,都会让你误判方向。

今天这篇文章,我就结合生产环境的真实案例,聊聊 CPU 飙高排查中最容易踩的 6 个坑,以及正确的排查姿势。


坑 1:看到 CPU 高,就以为一定是业务代码死循环

典型误判:很多人第一反应是"代码里有 while 死循环",然后开始逐行 review 代码。

实际情况:真实生产环境中,纯粹的死循环反而不常见。更常见的是:

  • 线程空转(等待资源但不断轮询)
  • 自旋重试(网络调用失败后不断重试)
  • 异常重试机制配置不当
  • 无效轮询(定时任务频率过高)

正确排查姿势

  1. 先看系统层 CPU:top 命令查看整体负载
  2. 再看 Java 进程 CPU:top -c 找到占用最高的 Java 进程
  3. 通过线程级分析定位热点线程:top -Hp <pid> 查看进程内各线程 CPU 占用
  4. 把线程 ID 转成十六进制,用 jstack <pid> | grep <线程 ID> 定位线程栈

核心原则:不要凭感觉猜,用数据说话。


坑 2:不会把 top、pidstat、jstack 串起来用

典型问题:很多人知道这些命令,但不知道如何串联使用,导致排查效率极低。

实战排查步骤

bash 复制代码
# 1. 找到 CPU 占用最高的 Java 进程
top -c

# 2. 查看该进程内各线程的 CPU 占用(假设 pid 是 12345)
top -Hp 12345

# 3. 找到占用最高的线程 ID(假设是 67890)
# 把线程 ID 转成十六进制
printf "%x\n" 67890

# 4. 用 jstack 定位该线程的堆栈
jstack 12345 | grep -A 30 <十六进制线程 ID>

关键点:线程 ID 必须转成十六进制才能在 jstack 中对应上。


坑 3:频繁 Full GC 或 Young GC,把 CPU 偷偷打满了

典型场景:CPU 很高,但业务代码看起来没问题,线程栈也正常。这时候很可能是 GC 在"背锅"。

常见原因

  • 对象创建过快,大量短命对象
  • 缓存失控,内存中堆积大量对象
  • 反射/序列化频繁分配对象
  • 大对象直接进入老年代,触发 Full GC

排查方法

bash 复制代码
# 查看 GC 情况
jstat -gcutil <pid> 1000 10

# 查看 GC 日志(如果开启了)
# 关注 Full GC 频率和耗时

联动思考:这个问题和我之前写的《Java 后端开发中的内存泄漏问题:90% 开发者都会踩的 5 个坑》是相关联的。CPU 高有时候只是表象,本质可能是内存分配问题。


坑 4:日志打太猛,CPU 都浪费在字符串拼接和 IO 上了

典型场景:代码里到处都是日志,尤其是循环内打印、debug 日志未关闭、JSON 序列化后再打印。

容易被忽略的点

即使日志级别没输出,字符串拼接的开销已经产生了。比如:

java 复制代码
// 即使 DEBUG 级别不输出,toString() 和 + 拼接已经消耗 CPU
log.debug("用户信息:" + user.toString() + ", 订单:" + order.toString());

排查方法

  1. jstack 查看是否有大量线程停在日志相关方法上
  2. 用性能分析工具(如 JProfiler、Async Profiler)查看 CPU 热点
  3. 检查日志配置,确保生产环境关闭 debug 日志

避坑建议

  • 生产环境日志级别至少是 INFO
  • 循环内禁止打印日志
  • 使用参数化日志代替字符串拼接:log.debug("用户信息:{}", user)

坑 5:线程池配置不合理,导致大量线程竞争和上下文切换

典型问题:很多人认为"线程越多越好",于是把线程池核心参数调得很大。

实际情况:线程不是越多越好。线程数过高会导致:

  • 大量线程竞争 CPU 时间片
  • 频繁的上下文切换(Context Switch)
  • 业务线程和 IO 线程混用,互相阻塞

排查方法

bash 复制代码
# 查看系统上下文切换次数
vmstat 1 10

# 查看 Java 线程数
jstack <pid> | grep "java.lang.Thread" | wc -l

配置建议

  • CPU 密集型任务:线程数 = CPU 核数 + 1
  • IO 密集型任务:线程数 = CPU 核数 * 2 或根据实际压测调整
  • 业务线程池和 IO 线程池分开配置

坑 6:热点锁竞争或 CAS 自旋,把 CPU 白白烧掉

技术含量较高的坑:CPU 高不一定是"业务很忙",也可能是"线程都在抢同一把锁"。

常见场景

  • synchronized 锁竞争
  • ReentrantLock 热点竞争
  • 原子类(AtomicInteger 等)的 CAS 自旋
  • 共享资源访问过于集中

排查方法

  1. jstack 查看是否有大量线程处于 BLOCKED 状态
  2. 用性能分析工具查看锁等待时间
  3. 检查代码中是否有热点资源被多线程频繁访问

避坑建议

  • 减少锁的粒度,能分段就分段
  • 避免在锁内执行耗时操作
  • 考虑使用无锁数据结构(如 ConcurrentHashMap)
  • 热点计数器可以考虑用 LongAdder 代替 AtomicLong

三、总结:CPU 飙高排查的正确路径

线上 CPU 飙高最怕的不是高,而是盲查。

很多人一上来先重启、先扩容、先调线程池参数,最后问题反复出现。真正有效的排查路径,应该是:

第一步:确定问题层级

  • 系统层 CPU 高?→ 检查是否有其他进程占用
  • JVM 进程 CPU 高?→ 继续往下排查
  • 某个线程 CPU 高?→ 定位线程栈

第二步:逐层收缩范围

  • 线程栈分析 → 定位业务代码
  • GC 分析 → 排除内存分配问题
  • 日志检查 → 排除无效开销
  • 锁竞争分析 → 排除同步问题

第三步:针对性解决

  • 代码问题 → 优化逻辑
  • 配置问题 → 调整参数
  • 架构问题 → 考虑拆分或缓存

CPU 高只是现象,找到让 CPU 白白浪费掉的那个地方,才是解决问题的关键。


系列文章回顾

下期预告:Java 后端生产问题系列还在继续,下一期聊聊"接口响应慢"的排查思路。

相关推荐
Honmaple2 小时前
阿里云 Coding Plan 终极全栈开发指南:Claude Code 与 OpenCode 模型配置全攻略
后端
农夫山泉不太甜2 小时前
Node.js 后端服务 Socket 优化深度指南:从基础到 IM 通信实战
前端·后端
农夫山泉不太甜2 小时前
NestJS 框架 Socket 优化实战指南
前端·后端
傲文博一2 小时前
Microsoft Remote Desktop 能连 Mac 吗?把 Mac 远程 Mac 这件事讲透
后端
烛衔溟2 小时前
TypeScript 类型别名、字面量类型、联合类型与交叉类型
前端·javascript·typescript·联合类型·类型别名·字面量类型·交叉类型
clamlss2 小时前
💥 踩坑实录:MapStruct 映射失效?揭秘 Lombok 组合下的编译期陷阱
java·后端
Cache技术分享2 小时前
369. Java IO API - DOS 文件属性
前端·后端
元俭2 小时前
【Eino 框架入门】Middleware 中间件:给 Agent 加一层"异常保护罩"
后端