【无标题】

markdown 复制代码
# Java 火焰图使用指南:CPU / 分配 / 锁 / 慢请求怎么查

@[TOC]

火焰图用来定位 **CPU 热点、分配垃圾、锁等待、慢请求卡在哪条调用链**,不是功能排查手册。本文按 Java 8 + Spring(含 Spring Cloud Gateway)来写,可直接对照采集和改法。

先有症状,再出图。没有对照的火焰图,宽段可能只是正常热点(例如 Jackson 解析本身就会宽)。

## 1. 火焰图是什么

火焰图把一段时间内的 **采样栈** 聚合成一张图:

**宽度**:成本。CPU 图是占用处理器的时间;分配图是分配的字节(或次数)。

**高度**:调用栈深度。层数多不等于更贵。

**颜色**:一般无含义,只为区分帧。

**上下关系**:谁调用谁。先确认根在上还是在下。

常见两种朝向:

- **Icicle(冰柱)**:根在上、叶子在下。async-profiler 的 HTML 多是这种。
- **Flame(火苗)**:根在下、叶子在上。Brendan Gregg 经典样式。

读栈时跟着根走:从进程 / 线程入口读到具体方法。

```text
+-------------------------------------------------------------+
|                    GatewayFilter.filter                     |  业务入口
|         SignVerifyGatewayFilter         |  其它 filter      |
|    HandlerStrategies.withDefaults       |                   |  <-- 越宽越值得看
| Jackson2JsonDecoder 构造 | 其它 codec   |                   |  叶子
+-------------------------------------------------------------+

核心原则:先找最宽的平台,不要先盯最尖的塔。

2. 什么时候该出图

现象 先出哪张图 典型原因
CPU 打满、机器热 CPU 热点计算、多余拷贝、加解密、正则、自旋
CPU 不高,Young GC 勤、P99 漂、内存抖 分配 alloc 热路径反复 new、每次新建 codec / ObjectMapper
线程多、吞吐上不去、请求大面积卡住 lock / wall 锁竞争、等 I/O、等下游
单次请求偶发超时 链路追踪,不是火焰图 火焰图是聚合采样,容易漏偶发
内存持续涨、疑似泄漏 Heap dump 分配图只看分配速率,不看谁没释放
功能错误、NPE、错单 日志 / 单测 火焰图不查对错

3. 四种常用图

3.1 CPU 火焰图

宽代表占用 CPU 的采样次数。

适合查:算得慢、解释执行、系统态 syscall、CAS / 自旋空转。

不适合单独查:线程在等锁、等 Redis、等 HTTP。CPU 是闲的,图上几乎没有。

3.2 分配火焰图(alloc)

宽代表分配了多少字节(或按次数统计)。

适合查:乱 new、GC 压力、网关读 body 时反复建 codec。

典型栈:构造器、byte[]StringBuilderJackson2JsonDecoder 的初始化。

3.3 lock 火焰图

宽代表等锁时间。

适合查:synchronized / ReentrantLock 竞争、连接池 / 缓存锁打满。

3.4 wall 火焰图(墙上时钟)

宽代表包含睡眠和 I/O 等待的真实耗时。

适合查:慢请求卡在 epoll、DNS、下游 HTTP、Thread.sleep

和 CPU 的区别:CPU 只看"在跑",wall 看"在等"。

4. 怎么读(30 秒流程)

  1. 确认朝向:根在上还是在下。
  2. 找最宽的框,同一层里越宽越优先。
  3. 从根往叶子读:哪段业务 → 哪次调用 → 哪个 JDK / 框架方法。
  4. 定位自己的类:真正该改的通常是中间那层业务方法,不是叶子上的 arraycopy / HashMap.put
  5. 问一句:这一层能不能少做,或做成一次?
    • 无状态对象每次 new → 复用(成员变量 / 容器 Bean)
    • 同样计算反复做 → 缓存
    • 不该在请求里做的 I/O → 挪到启动或异步
  6. 点框下钻,用 Ctrl / Cmd + F 搜类名,确认改前改后宽度变化。

容易看错的点

误区 正确理解
高度当成本 栈深 30 层但很瘦,不如 3 层很宽
只看叶子 叶子常是 JDK;改中间自己的方法
框架很宽就怪框架 先看是每个请求 new Decoder,还是真的在解析超大 JSON
图上全是 idle / health 流量没打到热路径,换压测接口
很窄的框当问题 采样有噪声;稳定很宽才信
一张图定论 要有对照:改前改后,或压测 A/B

5. 工具怎么选

面向 Java 8 + Spring 的常见组合:

工具 CPU 分配 Java 8 适用
async-profiler 首选,直接出 HTML
Arthas profiler 线上 attach,不用自己拷 so
JFR + JMC / jfrconv JDK 11+ 再作为主力
perf + FlameGraph 可以 Linux 系统 / CPU,不看 Java 分配
YourKit / JProfiler / IDEA Profiler 本地 GUI,生产一般不挂

结论:Java 8 用 async-profiler 或 Arthas。OpenJDK 8 通常没有可用 JFR。

macOS 本地 CPU 一般可用;分配图在 Linux 上更准。要拍板值不值得改,用和线上同架构的 Linux 打 alloc。

6. 采集命令

先找进程:

bash 复制代码
jps -l
# 或
ps -ef | grep java

下面命令里的 PID 换成目标进程号。

6.1 async-profiler

bash 复制代码
# CPU
asprof -e cpu   -d 30 -f cpu.html   PID

# 分配(按字节,查 GC / 乱 new 用这个)
asprof -e alloc -d 30 -f alloc.html PID

# 分配按次数(看 new 了多少次)
asprof -e alloc --alloc 1 -d 30 -f alloc-count.html PID

# 锁
asprof -e lock  -d 30 -f lock.html  PID

# 墙上时钟(慢请求)
asprof -e wall  -d 30 -f wall.html  PID

旧版脚本等价:

bash 复制代码
./profiler.sh -e cpu   -d 30 -f cpu.html   PID
./profiler.sh -e alloc -d 30 -f alloc.html PID

浏览器打开 HTML:点击帧下钻,搜索 HandlerStrategiesSignVerifyGatewayFilter 等类名。

容器注意:

  • 需要 perf_event 权限(或 privileged / 合适的 seccomp)。
  • kernel.perf_event_paranoid 过高会导致 CPU 栈缺失。
  • 缺 libc 符号时系统态帧会变成 [unknown]

6.2 Arthas

bash 复制代码
java -jar arthas-boot.jar
# 选中目标 PID

profiler start --event cpu
# 压测 30 秒
profiler stop --file /tmp/cpu.html

profiler start --event alloc
profiler stop --file /tmp/alloc.html

profiler start --event wall
profiler stop --file /tmp/wall.html

适合已经在跑的 Java 8 进程:attach 即可,不必重启。

6.3 JFR(JDK 11+)

bash 复制代码
jcmd PID JFR.start name=gw settings=profile duration=30s filename=/tmp/gw.jfr

# 转火焰图(async-profiler 转换器)
jfrconv --cpu   /tmp/gw.jfr cpu.html
jfrconv --alloc /tmp/gw.jfr alloc.html

或用 JDK Mission Control 打开 .jfr,看 Method Profiling / TLAB Allocation。

6.4 采集时注意

  • 先打热路径流量再采。 只打健康检查,图上全是 idle。
  • 时长建议 20 到 60 秒。太短噪声大,太长文件大且热点被冲淡。
  • 生产采集开销通常可接受,仍避免在已经 CPU 100% 的机器上长时间开 alloc。
  • 同一场景采两张:一张 CPU,一张 alloc,避免只看一种。

7. 场景、判断和改法

场景 A:CPU 打满

怎么看

  • CPU 图上某业务方法稳定很宽。
  • 叶子常是加密、JSON、正则、大循环、System.arraycopy

怎么改

栈上看到 处理
每次请求 MessageDigest.getInstance / 新建 cipher 复用或用 ThreadLocal
超大 JSON 反复序列化 缩小字段、缓存、换流式
复杂正则在热路径 预编译 Pattern,避免 String.matches
深拷贝、重复组装 DTO 少拷一次,原地改
Unsafe / CAS 空转很宽 可能是自旋锁,转 lock 图确认

改完用同一压测再出 CPU 图,目标帧应变窄。

场景 B:CPU 不高,GC 勤、P99 漂

怎么看

  • alloc 图,不要只看 CPU。
  • 宽段往往是构造器、byte[]ArrayListStringBuilder
  • 中间出现自己的 Filter / Interceptor / 工具类,且和某个构造几乎一样宽,说明热路径在造对象。

Gateway 典型:每次请求 HandlerStrategies.withDefaults()

优化前分配栈类似:

text 复制代码
SignVerifyGatewayFilter.lambda$apply$...
  HandlerStrategies.withDefaults
    DefaultHandlerStrategiesBuilder.build
      Jackson2JsonDecoder 构造
      FormHttpMessageReader 构造
      ...

SignVerifyGatewayFilterwithDefaults 几乎同宽,说明这个 Filter 的分配几乎全是在建 codec,不是验签逻辑。

怎么改

  • 构造时注入 ServerCodecConfigurermessageReaders 做成成员变量。
  • 同类问题:每次 new ObjectMapper()、每次 HandlerStrategies.withDefaults()、每次 new 线程池。
  • 判断标准:对象有没有请求级状态?没有就启动时建一次。

改完 alloc 图上 withDefaults 应消失(或只在启动出现),对应 Filter 整段变窄。

场景 C:吞吐上不去,线程很多,请求卡住

怎么看

  1. jstack / Arthas thread,看是 BLOCKED、WAITING 还是 TIMED_WAITING。
  2. lock 图:宽在 ReentrantLock.lock / synchronized,就是锁竞争。
  3. wall 图:宽在 socketRead、Redis 客户端、HttpClient、epollWait,就是等 I/O。
  4. CPU 图很瘦、wall 很宽 = 没在算,在等。

怎么改

栈上看到 处理
全局锁、大 synchronized 缩小锁粒度,或改并发数据结构
连接池 borrow 很宽 加大池、查泄漏、查慢 SQL 占连接
Redis / HTTP 客户端很宽 超时、批量、缓存、下游容量
Thread.sleep / 同步等待异步结果 改响应式或真正异步,避免阻塞 reactor 线程
Gateway 在 reactor 线程做阻塞调用 挪到 boundedElastic 或独立线程池

网关尤其危险:阻塞了 event loop,CPU 图可能不热,wall 图会爆。

场景 D:启动后一段时间 CPU 高,之后恢复

怎么看

  • CPU 图上解释器 / C1 编译、某个冷方法刚被调用。
  • 或启动期做全量预热、刷缓存、限流组件采集。

怎么改

  • 预热关键路径(第一次请求不要当压测)。
  • 启动宽限(例如系统规则不要在 CPU 爬升期误杀)。
  • 把可延后的初始化移出关键请求路径。

场景 E:系统态(灰色 / 内核帧)很宽

怎么看

  • CPU 图出现 copy_to_usertcp_sendmsgepoll_waitdo_sys_write
  • Java 帧相对窄,内核帧宽。

怎么改

  • 日志 / 访问日志写盘过猛:降级、异步、采样。
  • 过小的读写、过多的 syscall:缓冲、批量。
  • 网络带宽或连接数打满:扩容或合并请求。
  • 确认不是采集本身缺 Java 符号。栈全是 [unknown] 时先修符号,再下结论。

场景 F:Jackson / Fastjson 很宽

不要直接判定"换 JSON 库"。先分清:

宽在哪 含义 处理
Jackson2JsonDecoder 构造 每个请求 new decoder 复用 HttpMessageReader
ObjectMapper 构造 每次 new mapper 注入单例 ObjectMapper
readValue / parse 本身 真的在解析大 body 限流 body 大小、DTO 瘦身
Fastjson JSON.parseObject 在 Filter 入站解析,可能还有安全问题 按规范改 Jackson,并避免重复解析

8. 推荐工作流

text 复制代码
1. 描述症状:CPU / GC / 卡住 / P99
2. 选事件:cpu | alloc | lock | wall
3. 对热路径压测 20 到 60 秒(不要只打 health)
4. 打开 HTML,找最宽且含自己类名的帧
5. 判断:少做、做一次、还是挪走
6. 改代码
7. 同样条件再采一张,对比宽度

对照标准:

  • 目标帧明显变窄,或从热路径消失。
  • 业务指标同步改善:GC 次数、P99、CPU%。
  • 没有把成本挪到另一条更宽的栈上。

9. Gateway 对照例:复用 HTTP Message Readers

网关读 body 的错误写法(热路径每次建一套 codec):

java 复制代码
ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());

推荐写法(启动时复用容器里的 readers):

java 复制代码
private final List<HttpMessageReader<?>> messageReaders;

public XxxBodyFilter(ServerCodecConfigurer codecConfigurer) {
    this.messageReaders = codecConfigurer.getReaders();
}

// 请求处理里
ServerRequest.create(exchange, messageReaders);

如何发现(不一定先上火焰图):

  1. HandlerStrategies.withDefaults,看是否在 filter / apply 里。
  2. 点进源码:withDefaults()builder().build(),每次 new 全套 Jackson / Form / Multipart reader。
  3. 对象无请求级状态,又在热路径每次 new,就该复用。
  4. 用 alloc 图定量:withDefaults 宽度约等于该 Filter 总宽度,就可以改。

同类热路径还有:开放接口鉴权 Filter、入站 JSON 巡检 Filter、内部探测 Filter。处理方式一样,都是构造期缓存 messageReaders

代码审查时可固定搜:

bash 复制代码
rg "HandlerStrategies.withDefaults"
rg "new ObjectMapper\\("

出现在请求处理路径上,优先当分配问题看。

10. 速查

我想查 命令要点 图上找
谁在烧 CPU asprof -e cpu 最宽的业务方法
谁在狂 new asprof -e alloc 构造器、byte\[\]、自己的 Filter
谁在等锁 asprof -e lock lock / synchronized
请求卡在哪 asprof -e wall socket / Redis / HTTP / sleep
线上不便拷工具 Arthas profiler start --event ... 同上

一句话:先按症状选 CPU 还是分配(或 lock / wall),找稳定很宽、栈里有自己类名的那一层;无状态的反复创建就复用,算得慢就少算,卡住就去看锁和 I/O。

复制代码
脱敏对照(只给你看,文里没有真名):

| 原文 | 文中 |
|---|---|
| `AppSignGatewayFilterFactory` | `SignVerifyGatewayFilter` |
| `FuturesOpenGatewayFilterFactory` 等业务 Filter | 「开放接口鉴权 Filter」等描述,或 `XxxBodyFilter` |
| `AppSign` 缩写 | 已去掉 |
| Jedis 等可推断客户端的,场景 C 改成「Redis 客户端」 |
| Sentinel | 「限流组件 / 系统规则」 |
相关推荐
新时代牛马2 小时前
PCI与PCIe 硬件原理、配置空间/BAR 与 Linux 驱动完整篇:从 LTSSM、TLP 到 ECAM 与probe
java·linux·服务器
昇腾知识体系2 小时前
昇腾 950 RegBase 性能优化:从 msprof 采数到优化手法
人工智能·华为·性能优化·知识图谱
Bs_MoneyMagnet2 小时前
基于springboot+vue的医疗健康便民服务平台的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring
sunshine22 girl3 小时前
Java学习一 环境配置2 Idea的入门使用和maven配置,项目启动脚本配置
java·maven
有点。4 小时前
*C++哈夫曼树与哈夫曼编码
java·c++·servlet
CRMEB系统商城5 小时前
CRMEB标准版系统(Java)v3.1正式发布
java·spring·微信小程序·php·教育电商
写后端的胖头鱼5 小时前
【高频面试题】Java 基础类型 + 包装类 + String 互相转换
java·开发语言
weixin_460443565 小时前
企业考试系统从.NET迁移到Java+Linux:数据库迁移、接口兼容与灰度切换实践
java·煤矿培训考试系统·煤矿在线考试系统·煤矿安全培训·煤矿考试系统·一人一档
香菜TTT5 小时前
Redis的哨兵机制
java·数据库·redis