一、问题场景:CPU 飙高,但原因不明
服务没挂、内存也正常、数据库连接池也没满,但机器 CPU 突然飙到 90% 以上,请求延迟持续上升。
很多人第一反应是扩容、重启、加机器,但这些动作往往只是止血,不是定位问题。真正麻烦的是:CPU 高并不等于原因明显,很多时候线程栈、GC、频繁日志、错误重试、死循环、热点锁竞争,都会让你误判方向。
今天这篇文章,我就结合生产环境的真实案例,聊聊 CPU 飙高排查中最容易踩的 6 个坑,以及正确的排查姿势。
坑 1:看到 CPU 高,就以为一定是业务代码死循环
典型误判:很多人第一反应是"代码里有 while 死循环",然后开始逐行 review 代码。
实际情况:真实生产环境中,纯粹的死循环反而不常见。更常见的是:
- 线程空转(等待资源但不断轮询)
- 自旋重试(网络调用失败后不断重试)
- 异常重试机制配置不当
- 无效轮询(定时任务频率过高)
正确排查姿势:
- 先看系统层 CPU:
top命令查看整体负载 - 再看 Java 进程 CPU:
top -c找到占用最高的 Java 进程 - 通过线程级分析定位热点线程:
top -Hp <pid>查看进程内各线程 CPU 占用 - 把线程 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());
排查方法:
- 用
jstack查看是否有大量线程停在日志相关方法上 - 用性能分析工具(如 JProfiler、Async Profiler)查看 CPU 热点
- 检查日志配置,确保生产环境关闭 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 自旋
- 共享资源访问过于集中
排查方法:
- 用
jstack查看是否有大量线程处于 BLOCKED 状态 - 用性能分析工具查看锁等待时间
- 检查代码中是否有热点资源被多线程频繁访问
避坑建议:
- 减少锁的粒度,能分段就分段
- 避免在锁内执行耗时操作
- 考虑使用无锁数据结构(如 ConcurrentHashMap)
- 热点计数器可以考虑用 LongAdder 代替 AtomicLong
三、总结:CPU 飙高排查的正确路径
线上 CPU 飙高最怕的不是高,而是盲查。
很多人一上来先重启、先扩容、先调线程池参数,最后问题反复出现。真正有效的排查路径,应该是:
第一步:确定问题层级
- 系统层 CPU 高?→ 检查是否有其他进程占用
- JVM 进程 CPU 高?→ 继续往下排查
- 某个线程 CPU 高?→ 定位线程栈
第二步:逐层收缩范围
- 线程栈分析 → 定位业务代码
- GC 分析 → 排除内存分配问题
- 日志检查 → 排除无效开销
- 锁竞争分析 → 排除同步问题
第三步:针对性解决
- 代码问题 → 优化逻辑
- 配置问题 → 调整参数
- 架构问题 → 考虑拆分或缓存
CPU 高只是现象,找到让 CPU 白白浪费掉的那个地方,才是解决问题的关键。
系列文章回顾:
下期预告:Java 后端生产问题系列还在继续,下一期聊聊"接口响应慢"的排查思路。