大家好,我是程序员天天困。
这是「Arthas 线上诊断实战」系列第 5 篇,专门讲 Arthas Profiler 火焰图——怎么在 JVM 里采样出一张火焰图,以及火焰图到底该怎么读。前四篇 watch、trace、vmtool、mc+retransform 解决的是「功能 bug」,从这篇开始,我们碰「性能问题」。点个收藏,直接上手。
一、CPU 飙了,trace 一层一层追不够用了
当 CPU 水位飙到 90% 以上,最怕的不是没有工具,而是工具看得太窄。
前几篇里 watch 和 trace 负责定位问题——watch 看入参返回值,trace 看方法调用链的耗时分布。但碰到 CPU 飙高这种场景,trace 有个天生短板:一次只能盯一条方法链。你 trace 一个接口入口,它告诉你这个方法调了哪些子方法、各花了多久。可问题是——线上 CPU 飙高的时候,消耗往往不是集中在一个方法里,而是分散在几十条调用路径上。
举个例子:某个报表服务 CPU 突然飙到 95%,告警群里连着 @。拿 trace 追接口入口,结果入口方法本身耗时正常,真正的 CPU 消耗被拆散在十几个子方法里。trace 一条条追,追了五六条还没抓到重点——那种感觉就是拿手电筒在黑屋子里照路,照到哪是哪,看不到全貌。

这时候就得换思路了:不追单条链,直接用 profiler 对整个 JVM 采样,生成一张火焰图。图一出来,最宽的那条「火苗」直接指向一个藏在循环里的 Pattern.compile——这才是真正的元凶。
trace 的定位是「精确到某条链路」;profiler 的定位是「高空俯瞰、看全局哪里最热」。两者不矛盾,是配合关系。
二、火焰图是什么:把调用栈剖开给你看
火焰图最大的价值,是给你一个全局视野——所有线程的调用栈在同一张图上摊开,谁占的面积大,谁就是热点。
火焰图(Flame Graph):由 Linux 性能优化专家 Brendan Gregg 发明的可视化方法,把程序运行期间的调用栈采样结果画成一张「火焰」形状的图。你可以把它理解为「给程序拍了一张全身 X 光片,哪个函数的骨头最粗一眼就能看到」。

async-profiler:一个低开销的 Java 采样分析工具,Arthas 的 profiler 命令底层就是基于它实现的。你不需要额外安装 async-profiler,Arthas 启动时就自带了。
采样(Sampling):性能分析的一种方式——不记录每一次方法调用,而是每隔固定时间间隔(默认 10ms)「拍一张调用栈快照」。你可以理解为「路口架了个摄像头,每隔几秒拍一张」,拍得多了自然就能统计出哪条路最堵。
读火焰图,记住三个要素就行:
横轴不是时间,是采样占比。 火焰图把采集到的所有调用栈按字母顺序横向排好,相同路径的栈合并在一起。一个格子从左到右的宽度,代表它在采样中出现的次数占比——越宽,说明这个函数(或它的子调用)占用的资源越多,越可能是瓶颈。注意,左右排列顺序只是为了聚合,不代表执行先后。
纵轴是调用栈深度。 下面是调用者(父函数),上面是被调用者(子函数)。栈越深,图越高,像火焰往上窜。
颜色没有特殊含义。 暖色调只是方便区分不同函数,红色不代表「有问题」,绿色也不代表「没问题」——你只需要看宽度。

三、Arthas profiler 三步出图:start → status → stop
Arthas 的 profiler 命令核心就三步:开始采样、看采样状态、停止并生成火焰图。
前提还是先把 Arthas Attach 到目标 JVM(前面几篇讲过,java -jar arthas-boot.jar 选进程进到 [arthas@PID]$)。进来之后:
第 1 步,启动采样: 默认采 cpu 事件,每 10ms 采一次。
profiler start
终端回显 Profiling started 就说明采样已经跑起来了。

第 2 步,看看采了多少了: status 看采样状态,getSamples 看已采集的样本数。
profiler status
profiler getSamples
status 会告诉你当前在采哪种事件、跑了多久;getSamples 直接给你一个数字——样本数太少(比如个位数)说明采的时间不够,结果没代表性。

第 3 步,停止采样,生成火焰图:
profiler stop --format flamegraph
成功后会打印输出文件路径,比如 /tmp/arthas-output/20260728-092813.html。
怎么打开看?Arthas 默认用 3658 端口提供 HTTP 访问,浏览器打开 http://localhost:3658/arthas-output/ 就能看到所有生成的火焰图文件,点击直接在浏览器里交互查看。Chrome 用户可能需要刷新一两次才能加载出来。

四、火焰图怎么读:先找最宽的那条火苗
读火焰图的口诀就一句:先找最宽的那条,然后顺着它往下看调用链。
拿到一张火焰图,别一上来就逐行读,那样会晕。正确的姿势是:
- 扫全局,找最宽的格子——它占的采样最多,就是你最该关注的热点。
- 往上看子函数——一个格子的宽度通常被它的子调用继承,真正在干活的往往是栈顶(最上面)那个。
- 往下看父函数链——理解这个热点是在什么业务场景下被触发的,才能判断该不该优化、怎么优化。
举个场景。假设 com.ttk 包里有个报表过滤服务,某天 CPU 直接打满:
// 示例:报表过滤服务,数据量大时 CPU 飙高
public class ReportFilterService {
public List<ReportItem> filter(List<ReportItem> items, String keyword) {
List<ReportItem> result = new ArrayList<>();
for (ReportItem item : items) {
// 问题:Pattern.compile 放在循环里,每次迭代都重新编译正则
if (Pattern.compile(keyword).matcher(item.getContent()).find()) {
result.add(item);
}
}
return result;
}
}
profiler 采样 60 秒后生成火焰图,最宽的一条赫然是 Pattern.compile——占了将近 40% 的宽度。往下看调用链:ReportFilterService.filter() → Pattern.compile()。原来正则编译被放在了循环里,数据量一大,光编译正则就把 CPU 吃干了。

修复也简单——把 Pattern.compile 提到循环外面只编译一次:
// 修复后:正则只编译一次
public List<ReportItem> filter(List<ReportItem> items, String keyword) {
Pattern pattern = Pattern.compile(keyword);
List<ReportItem> result = new ArrayList<>();
for (ReportItem item : items) {
if (pattern.matcher(item.getContent()).find()) {
result.add(item);
}
}
return result;
}
这就是火焰图的价值:它不需要你猜,图上最宽的那条直接告诉你答案。
可能有人会问:火焰图里有一大片
unknown/[unknown]是什么意思?unknown 通常出现在 native 帧或 JIT 编译的代码上——采样时拿不到对应的符号信息。如果 unknown 占比很小(比如 < 5%),直接忽略。如果占了一大块,可能是 async-profiler 的 C 栈收集方式没配对,试试加
--cstack fp(用 Frame Pointer 方式收集 native 栈)换一种模式重新采样。
五、不止 CPU:alloc / lock / wall 切事件
profiler 不只能看 CPU,换一个 --event 参数就能查内存分配、锁竞争、阻塞时间。
先用 profiler list 看看当前平台支持哪些事件:
profiler list
下面是我在 macOS 上跑出来的结果(Linux 上会多出 ctimer 和一批 perf events,比如 cache-misses、branches 等):
Basic events:
cpu
alloc
nativemem
lock
nativelock
wall
itimer
日常最常用的是 cpu、alloc、lock、wall 这几个,itimer 作为补充:
| event | 看什么 | 典型场景 |
|---|---|---|
| cpu | CPU 占用采样 | CPU 飙高,找最耗 CPU 的函数 |
| alloc | Java 内存分配量 | 频繁 GC、堆涨太快、找谁在疯狂 new 对象 |
| lock | Java 锁竞争耗时 | 线程阻塞、响应变慢、锁粒度太大 |
| wall | 墙钟时间(含等待) | 请求整体慢但不一定是 CPU 问题 |
| itimer | CPU(备选方案) | 容器里 perf_event 不可用时替代 cpu |
nativemem和nativelock分别看 native 层的内存分配和锁竞争,排查 JNI、第三方 native 库时才用得上,日常 Java 应用分析很少碰到。

切 event 的操作完全一样,只是 start 时多加一个参数。查内存分配热点就 --event alloc,查锁竞争就 --event lock:
profiler start --event alloc
profiler start --event lock
我的习惯是:CPU 飙高先上 cpu,GC 频繁就切 alloc,接口卡但 CPU 不高就试 wall 或 lock。
六、profiler 踩坑与进阶技巧
profiler 命令本身不难,真正决定结果质量的是采样时长和结果解读的判断力。
几个我实战中反复用到的技巧:
1)采样时长要够。 采 5 秒和采 60 秒,结果的代表性完全不一样。太短了可能恰好采到一段不典型的负载。我的经验是至少采 30 秒以上,业务有周期性波动的就覆盖一个完整周期。不想手动掐表的话,用 --duration 让它到点自动停。下面这条命令会在 120 秒后自动停止并生成火焰图:
profiler start --duration 120
2)--format md 直接出 Markdown 报告。 这是 Arthas 较新的功能,不用打开浏览器看 HTML,直接在终端拿到一份结构化的 Markdown 报告——包含 Top N 热点函数和调用树,方便贴到群里和同事讨论,或者丢给 AI 帮你分析:
profiler stop --format md
3)-t 分线程采样。 默认火焰图是把所有线程的栈混在一起的。如果你怀疑是某个特定线程在吃 CPU,加 -t 就能按线程分别出图,每条栈以线程名结尾:
profiler start -t
4)--include / --exclude 过滤噪音。 应用复杂时火焰图里会有一大堆框架代码(Spring、Jackson 之类),你可能只关心自己的业务代码。用 --include 只留关心的包路径,比如只看 com/ttk 包下的调用栈、排除 Unsafe.park(线程池 parked 状态的噪音):
profiler stop --include 'com/ttk/*' --exclude '*Unsafe.park*'
5)性能开销很低,但别长时间挂着。 async-profiler 是采样式的,不是字节码插桩,开销大约 1%~5%,生产环境完全可以接受。但我的建议是:采完就 stop,别让它一直跑着——长时间采样不仅结果文件变大,还占着 Arthas 的 agent 资源。

七、profiler vs trace:什么时候用哪个
profiler 和 trace 不是二选一,而是「先俯瞰定位区域,再 zoom in 看细节」的配合关系。
| 维度 | profiler | trace |
|---|---|---|
| 视野 | 全局,所有线程所有方法的采样 | 单个方法 / 类的方法调用链 |
| 看什么 | CPU / 内存 / 锁的采样占比 | 单次调用的逐层耗时 |
| 适合 | CPU 飙高、找全局热点在哪 | 已知入口,追某条链路哪层最慢 |
| 原理 | 定时采样,统计频率 | 每次方法进出都记录 |
| 开销 | 极低(采样式) | 高频方法有一定开销 |
我的推荐流程是:先用 profiler 俯瞰全局,定位到热点区域 → 再用 trace 精确追那一条链路的耗时分布 → 最后用 watch 或 vmtool 确认入参和对象状态。 三步走,从粗到细,不遗漏也不浪费时间。
结语
Arthas Profiler 火焰图给你的是一张「全局热力图」——哪里最宽,哪里就最值得优化。trace 帮你看清单条链路的细节,watch 帮你确认方法入参返回值,mc+retransform 帮你热修方法体,而 profiler 帮你在性能问题面前先看到全局。五篇下来,你的 Arthas 工具箱已经能覆盖「定位 → 分析 → 修复 → 验收 → 性能分析」的完整闭环了。
本文命令参数以 Arthas Profiler 官方文档为准,版本更新了以官网为准。
我是程序员天天困,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你有没有用火焰图揪出过意想不到的 CPU 热点?当时最宽的那条是什么函数?