总览

learn 索引 | 主题索引

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 件事

  1. 宽度是成本占比。
  2. 高度是调用深度。
  3. 先看最宽的少数几个块。
  4. 沿宽块往下追业务来源。
  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 库。要追问:

  1. 谁调用了它?
  2. 输入规模多大?
  3. 是否被循环调用?
  4. 是否能批量处理?
  5. 是否应该做资源隔离?

7. 先别学什么

初学火焰图时,先不要陷入这些内容:

  • 每一种 profiler 的底层采样机制差异。
  • perf event 的所有参数。
  • JIT 内联后的所有符号解释。
  • Kernel 栈和用户态栈的完整展开。
  • 火焰图颜色主题含义。

这些不是不重要,而是前期投入产出比低。先把“宽块优先、沿栈追源、按图类型判断”练熟,更符合帕累托法则。

8. 实战判断口诀

先看类型,再看宽度。
宽块优先,颜色其次。
栈顶是现象,栈底找来源。
CPU 看计算,Wall 看等待,Alloc 看对象。
优化要复测,别凭感觉收工。

9. 一个典型例子

假设 CPU 火焰图最宽路径是:

业务音频处理
-> Opus 解码
-> PCM 缓冲
-> MP3 编码
-> Native 写入

不要只说:

Native 写入很耗 CPU

更好的结论是:

CPU 热点集中在音频转码链路。需要继续确认单次音频输入大小、转码并发、缓冲结构是否产生装箱和数组搬移,以及是否应从主链路拆出独立处理。

这就是帕累托视角:优先抓住最大成本路径,并把优化动作对准能明显缩短宽度的因素。