总览
1. 一句话理解
火焰图不是用来“欣赏调用栈”的,而是用来回答一个问题:当前性能成本主要花在哪几条调用路径上,哪 20% 的代码贡献了 80% 的 CPU、等待或内存分配成本。
帕累托法则在火焰图里的用法是:
先找最宽的少数几块
-> 再追它们下面的调用来源
-> 只优化能显著缩短宽度的路径
-> 避免陷入细枝末节2. 火焰图看什么
flowchart TD A[一次采样数据] --> B[合并相同调用栈] B --> C[生成火焰图] C --> D[横向宽度代表占比] C --> E[纵向高度代表调用深度] C --> F[顶部通常是正在执行的方法] D --> G[优先看最宽路径]
2.1 横向宽度
横向宽度表示某段调用栈在采样中出现的比例。越宽,说明越常被采到,也就是越值得关注。
重点:宽度代表成本占比,不代表方法执行顺序。
2.2 纵向高度
纵向高度表示调用深度。下层通常是调用者,上层通常是被调用者。
重点:高不一定有问题,宽才代表成本集中。
2.3 颜色
大多数火焰图颜色只是为了区分块,不一定表示严重程度。除非工具明确说明颜色含义,否则不要把红色直接理解为危险。
3. 帕累托分析法
3.1 第一步:先看最宽的 3 到 5 个块
不要从最左边、最上面或颜色最红的块开始看。正确做法是找最宽的块:
最宽块 = 采样占比最高 = 最值得先分析如果一个方法占比 35%,另一个占比 1%,即使后者代码看起来更丑,也应该先看 35% 的路径。
3.2 第二步:沿着宽块向下追调用来源
看到宽块后,不要只记录栈顶方法,还要沿着它往下看调用链:
业务入口
-> 框架调度
-> 业务方法
-> 工具方法
-> native 或系统调用性能优化通常要改的是“业务入口和调用方式”,而不一定是最顶部那个库函数。
例如栈顶是:
ShortPointer.put真正要问的是:
谁在大量调用它?
为什么一次任务要写这么多样本?
有没有缓冲结构或数据规模放大?3.3 第三步:判断热点类型
| 热点形态 | 常见含义 | 下一步 |
|---|---|---|
| 业务方法很宽 | 业务逻辑本身耗时 | 看算法、循环、数据规模 |
| JSON 序列化很宽 | 大对象或高频序列化 | 看字段、次数、缓存 |
| 正则很宽 | 正则复杂或被高频调用 | 简化规则或预编译 |
| 锁等待很宽 | 线程争用 | 看锁范围和共享资源 |
| IO 等待很宽 | 下游慢或连接池不足 | 看数据库、HTTP、Redis |
| GC 或分配很宽 | 对象创建过多 | 看集合、装箱、大对象 |
| native 方法很宽 | JNI、压缩、加密、编解码 | 追业务调用来源和输入规模 |
3.4 第四步:只做能缩短宽块的优化
性能优化不是“看到问题都修”,而是优先修会改变图形宽度的地方。
应该优先:
- 降低高占比路径的调用次数。
- 降低高占比路径的单次成本。
- 减少高占比路径的输入规模。
- 把高占比 CPU 任务隔离到独立资源。
暂时不要优先:
- 优化占比很低的方法。
- 重构与热点无关的代码。
- 根据代码洁癖做性能优化。
- 只因为某个函数名字像问题就下结论。
4. 最短上手路径
4.1 你只需要先学 5 件事
- 宽度是成本占比。
- 高度是调用深度。
- 先看最宽的少数几个块。
- 沿宽块往下追业务来源。
- 根据火焰图类型判断 CPU、等待还是内存分配。
4.2 一次标准阅读流程
flowchart TD A[打开火焰图] --> B[确认图类型] B --> C[找最宽的块] C --> D[记录栈顶方法] D --> E[向下追调用来源] E --> F[判断热点类型] F --> G[提出验证假设] G --> H[改一个关键点] H --> I[重新采样对比]
4.3 标准记录模板
## 火焰图结论
- 图类型:CPU / Wall-clock / Allocation
- 采样时间:
- 进程:
- 最宽热点:
- 热点占比:
- 业务调用来源:
- 初步判断:
- 下一步验证:
- 优化后对比:5. 常见火焰图类型
5.1 CPU 火焰图
回答:CPU 时间花在哪里?
适合排查:
- CPU 飙高。
- 算法慢。
- 编解码、压缩、加密耗时。
- JSON 或正则高频执行。
如果 CPU 火焰图里某个业务方法很宽,说明它真的在消耗 CPU。
5.2 Wall-clock 火焰图
回答:整体耗时卡在哪里?包括等待。
适合排查:
- 接口响应慢但 CPU 不高。
- 数据库、Redis、HTTP 下游慢。
- 锁等待。
- 线程池排队。
Wall-clock 火焰图中很宽的 IO 或锁等待,不代表 CPU 忙,而代表时间被等待吃掉。
5.3 Allocation 火焰图
回答:对象是谁分配出来的?
适合排查:
- Young GC 频繁。
- G1 Refine 活跃。
- 内存分配速度高。
- 大量装箱、集合扩容、字符串拼接。
如果分配火焰图里 ArrayList、装箱、JSON 对象很宽,说明优化方向是减少对象创建,而不是盲目调大堆。
6. 高频场景
6.1 CPU 被打满
优先看 CPU 火焰图。
判断路径:
最宽 CPU 块
-> 是否是业务代码
-> 是否是循环或大输入
-> 是否是 native 编解码
-> 是否可以限流或异步隔离6.2 接口慢但 CPU 不高
优先看 Wall-clock 火焰图。
判断路径:
最宽等待块
-> 数据库或 Redis
-> HTTP 下游
-> 锁等待
-> 线程池排队6.3 GC 压力大
优先看 Allocation 火焰图。
判断路径:
最宽分配块
-> 集合扩容
-> 装箱拆箱
-> JSON 序列化
-> 字符串拼接
-> 大数组复制6.4 看到 native 方法很宽
不要直接怪 native 库。要追问:
- 谁调用了它?
- 输入规模多大?
- 是否被循环调用?
- 是否能批量处理?
- 是否应该做资源隔离?
7. 先别学什么
初学火焰图时,先不要陷入这些内容:
- 每一种 profiler 的底层采样机制差异。
- perf event 的所有参数。
- JIT 内联后的所有符号解释。
- Kernel 栈和用户态栈的完整展开。
- 火焰图颜色主题含义。
这些不是不重要,而是前期投入产出比低。先把“宽块优先、沿栈追源、按图类型判断”练熟,更符合帕累托法则。
8. 实战判断口诀
先看类型,再看宽度。
宽块优先,颜色其次。
栈顶是现象,栈底找来源。
CPU 看计算,Wall 看等待,Alloc 看对象。
优化要复测,别凭感觉收工。9. 一个典型例子
假设 CPU 火焰图最宽路径是:
业务音频处理
-> Opus 解码
-> PCM 缓冲
-> MP3 编码
-> Native 写入不要只说:
Native 写入很耗 CPU更好的结论是:
CPU 热点集中在音频转码链路。需要继续确认单次音频输入大小、转码并发、缓冲结构是否产生装箱和数组搬移,以及是否应从主链路拆出独立处理。这就是帕累托视角:优先抓住最大成本路径,并把优化动作对准能明显缩短宽度的因素。