IWA
2025-12-09
点 赞
0
热 度
2
评 论
0

Java 性能优化实战 第 6 篇:CPU 使用率飙高的根因定位(火焰图 + Linux 实战排查全流程)

本篇极其关键,因为 Java 服务 CPU 飙高 是最常见、最棘手、最容易误判的线上事故之一。

常见表现:

  • CPU 100%

  • 系统卡住,但无报错

  • RT 突刺

  • GC 频繁

  • 单个线程耗尽 CPU

  • 某些接口异常慢

90% 的开发者排查 CPU 问题时没有方向,本篇将把排查流程工具化、一步一步教你找到“罪魁祸首”。


一、为什么 CPU 飙高比 GC 问题更难排查?

因为 CPU 飙高常见的 3 类原因极其相似:

  1. 线程死循环(while(true))

  2. 锁竞争严重,线程疯狂自旋

  3. GC 导致 CPU 撑满

  4. IO 卡死导致线程大量等待(也会间接被误判)

  5. 热点方法被疯狂调用(高并发下)

这种情况下,日志一般没报错,也不会有 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 锁竞争分析


用键盘敲击出的不只是字符,更是一段段生活的剪影、一个个心底的梦想。希望我的文字能像一束光,在您阅读的瞬间,照亮某个角落,带来一丝温暖与共鸣。

IWA

infp 调停者

具有版权性

请您在转载、复制时注明本文 作者、链接及内容来源信息。 若涉及转载第三方内容,还需一同注明。

具有时效性

文章目录

IWA的艺术编程,为您导航全站动态

43 文章数
9 分类数
19 评论数
34标签数

访问统计