本篇极其关键,因为 Java 服务 CPU 飙高 是最常见、最棘手、最容易误判的线上事故之一。
常见表现:
CPU 100%
系统卡住,但无报错
RT 突刺
GC 频繁
单个线程耗尽 CPU
某些接口异常慢
90% 的开发者排查 CPU 问题时没有方向,本篇将把排查流程工具化、一步一步教你找到“罪魁祸首”。
一、为什么 CPU 飙高比 GC 问题更难排查?
因为 CPU 飙高常见的 3 类原因极其相似:
线程死循环(while(true))
锁竞争严重,线程疯狂自旋
GC 导致 CPU 撑满
IO 卡死导致线程大量等待(也会间接被误判)
热点方法被疯狂调用(高并发下)
这种情况下,日志一般没报错,也不会有 OOM,甚至 QPS 很高但 CPU 被打满。
所以必须依赖 Linux + JVM 工具链去定位根因。
二、CPU 100% 现场采集三板斧(极其重要)
如果服务器 CPU 快被打满,请立刻执行这三步:
① top -H -p 找到最忙线程
top -H -p 12345输出类似:
PID USER PR NI VIRT RES SHR S %CPU TIME+ COMMAND
12350 java 20 0 ... R 280 98.7 1:23 java注意:
这是 JVM 内部线程 ID(LWP)
CPU 高的通常只有 1~3 个线程
可以快速定位“问题线程”
② 将 LWP 转为十六进制(用于匹配线程栈)
假设 busy 线程 LWP = 12350
执行:
printf "%x\n" 12350输出:
303e这个十六进制值在 JVM thread dump 中会用到。
③ 执行线程栈 dump
jstack -l 12345 > jstack.log打开日志,搜索:
0x303e你就能找到 CPU 飙高的那个线程的完整调用栈。
🎉 至此,问题已经 80% 定位成功。
三、通过栈帧判断 CPU 根因(最关键)
下面教你如何从栈信息“读懂问题”。
场景 1:线程死循环(典型 CPU 100%)
jstack 会显示类似:
"thread-1" #45 prio=5 os_prio=0 cpu=98.23%
java.lang.Thread.run
com.xxx.LoopTask.run(LoopTask.java:88)
while(true) {...}
几乎 100% 是:
无 sleep
无阻塞
while(true) 空转
解决方案:
增加 sleep
使用 await/notify
使用阻塞队列代替轮询
场景 2:锁竞争激烈(线程被大量阻塞或自旋)
Stack 会看到:
"thread-12" BLOCKED on java.util.concurrent.locks.ReentrantLock$NonfairSync或者:
parking to wait for <0x00000007898d8e20> // 意味着 LockSupport.park这类情况属于典型 热点锁竞争。
根因包括:
synchronized 大量竞争
ReentrantLock 热点
Double-checked locking 锁粒度太大
多线程抢同一个 HashMap/Set
解决方式:
减小锁粒度(锁拆分)
使用 ConcurrentHashMap
尽量使用 CAS 结构(AtomicXXX)
使用 LongAdder 替代 AtomicLong
场景 3:GC 导致 CPU 撑满
堆满/混合 GC 打满 CPU 时,jstack 通常表现为:
"GC Thread#0" os_prio=2 cpu=90.23%或者 thread dump 中一堆:
G1YoungRemSetSampling
G1EvacuateRegions
ConcurrentMark解决方式:
扩大堆
降低 IHOP
缩短 Survivor→Old 晋升
参考第 5 篇 GC 调优方案
场景 4:大量线程等待 IO(误判为 CPU 高)
如果 thread dump 里一堆:
java.net.SocketInputStream.socketRead
sun.nio.ch.EPollArrayWrapper.epollWait说明是:
下游服务慢
数据库响应长
Redis 超时
ES 响应慢
CPU 并不是真的忙,而是 系统大量线程被阻塞造成吞吐下降 → CPU 低但业务慢。
解决:
增加超时
降低 maxThreads
用连接池
下游限流/隔离
使用线程池 + Bulkhead 隔离(Hystrix/Resilience4J)
场景 5:热点方法导致 CPU 飙高
dump 中可能显示:
at com.xxx.xxx.calculatePrice(Price.java:201)重复出现几十次。
说明:
高频调用的某个方法太慢
算法复杂度高(O(n^2))
复杂 JSON 解析
正则匹配过重
大量反射调用
解决:
算法优化
引入缓存
使用 fastjson2/Jsoniter 替代 Jackson
避免正则,改用状态机
预编译正则
四、CPU 问题终极武器:火焰图(FlameGraph)
当你需要完整的 CPU 调用链路,必须用到火焰图。
① 采集 perf 数据
sudo perf record -F 99 -p <pid> -g -- sleep 30② 生成火焰图
perf script > out.perf
stackcollapse-perf.pl out.perf > out.folded
flamegraph.pl out.folded > flame.svg打开 flame.svg,你能看到:
哪个函数消耗 CPU 时间最长
方法占比
调用链路
逻辑热点位置
🔥 火焰图是性能优化的最高级武器,没有之一。
五、CPU 性能优化 Checklist(生产可直接应用)
1. 避免线程死循环
不要 while(true) 空轮询
必须加 sleep 或 await
2. 减少锁竞争
拆分锁
用 CAS
用分段锁
用 ConcurrentHashMap
3. 合理配置线程池
CPU 密集:N+2
IO 密集:2N
队列不能太大
4. 降低对象创建频率
避免频繁 new(尤其是 JSON、StringBuilder)
引入对象池但谨慎使用
5. 避免滥用正则表达式
性能杀手
用预编译 Pattern
或者用手写匹配
6. 使用缓存减少重复计算
本地缓存 Caffeine
分布式缓存 Redis
预计算和预热
7. 优化数据库/下游服务调用
优化 SQL
索引必须正确
驱动层要开启连接池
下游调用要有超时与熔断
下一篇预告(第 7 篇)
Java 性能优化实战 30 讲(第 7 篇)
MySQL 性能优化与慢查询定位(索引、锁、执行计划实战)
内容包括:
MySQL 慢查询排查(explain + trace 实战)
覆盖索引、回表、索引失效的本质
Join、Group、Order By 的最佳写法
高频 SQL 语句的优化模板
线上常见 MySQL 性能事故定位方法
InnoDB 锁竞争分析
默认评论
Halo系统提供的评论