1. 线上CPU打满,为什么jstack常常给不了答案
先说一个我碰过无数次的场景:线上告警CPU飙到90%以上,登录机器,熟练地敲下 jstack -l pid,连续抓了好几次,然后对着满屏线程栈发呆。明明CPU被打满了,可每次抓栈都“雨露均沾”,没有哪个方法敢说自己占了资源。要么抓到的是一个正在等待的线程状态,要么抓到的是GC线程在忙,业务代码连影子都看不见。很多人第一反应是自己抓的时机不对,再多抓几次,但本质上,jstack能提供的只是一个极短的时间切片,不是统计意义上的热点分布。
后来我把Arthas的 profiler 命令用顺手,类似问题基本能在一张火焰图里锁定嫌疑帧。这套工具不是银弹,但面对“CPU高、卡顿、RT抖动”这类问题,它比jstack、jvisualvm、jstat组合拳要直观得多。这篇文章就把我用Arthas生成火焰图的完整链路写清楚,包括命令、参数、读图方法,以及生产环境下真正值得注意的边界。
1.1 瞬时切片与统计样本的差别
jstack抓的是“此时此刻”所有线程的栈。如果CPU热点方法是一个高频但每次执行时间极短的调用,它可能在你按下命令的那一刻根本不在栈上,抓十次未必能撞上一次。而火焰图靠的是持续采样:默认每10ms采一次,一分钟就积累约6000个调用栈样本,哪个方法在样本里出现得最多,它的矩形就最宽。这是统计规律,不是运气。
说白了,jstack适合回答“现在有没有死锁、有没有线程卡住”,火焰图适合回答“过去这段时间CPU到底在算什么东西”。两者定位不同,生产上经常是先用 dashboard 和 thread 做粗筛,再上火焰图做精确定位。
1.2 安全点问题:jstack不是免费的
还有一个很多人忽略的点:jstack抓线程栈需要让JVM进入安全点,然后把所有线程的栈一次性拷贝出来。这意味着抓栈的瞬间,整个应用会被短暂冻结。流量越高,冻结的影响越明显。线上业务如果正在打满CPU,你连续抓几次jstack,等于自己给服务补了几次抖动。
Arthas的 profiler 底层用的是 async-profiler,它通过操作系统的性能计数器按固定频率发中断,在中断处理函数里通过 JVM 的 AsyncGetCallTrace 接口,只采当前触发中断的那一个线程的调用栈。整个过程不需要目标线程进入安全点,也不需要停住其他线程。这就是为什么它能长时间高频率采样,而且对业务影响很小。
1.3 火焰图方案:一个更符合直觉的展示方式
火焰图把所有采样到的调用栈叠在一起:y轴是调用深度,x轴是样本占比。一眼看过去,哪个方法宽,资源就耗在哪。相比在终端里翻几百行线程栈,这种图形化方式对大脑友好太多。而且Arthas把整个流程压缩到了两条命令里:profiler start 和 profiler stop,下面从零开始走一遍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从启动到出图:profiler命令完整操作链
这一节是纯操作,照着做就能出图。我假设你的应用是Java进程,JDK8以上,机器上有网络,能下载一个jar包。
2.1 启动Arthas并选中目标进程
Arthas的启动方式是下载一个 arthas-boot.jar,然后运行:
bash复制curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
启动后它会列出当前机器上的Java进程,输入序号回车,就会attach到目标进程。正常情况下你会看到类似这样的输出:
text复制[INFO] arthas-boot version: 3.7.2
[INFO] Found existing java process, please choose one and input the serial number:
* [1]: 18432 com.example.OrderServiceApplication
输入 1 回车,进入Arthas控制台。这里有个小提醒:如果目标应用跑在容器里,Arthas需要能看到同一个进程命名空间,通常直接在容器内执行或者在共享PID命名空间的宿主机上执行。碰到attach不上,优先检查权限和进程可见性,而不是反复重启。
2.2 开始采样:profiler start怎么用
进入控制台后,直接执行:
code复制profiler start
默认情况下,它会以cpu事件、10ms间隔开始采样。你不需要在采样期间盯着控制台,让它跑着就行。Arthas在3.6以上版本已经内置了async-profiler支持,不用额外装native库,这点比早年省事很多。
我更常用的方式是加上时长参数,避免忘记stop:
code复制profiler start --duration 60
意思是采样60秒后自动停止采集,但停止后文件还没生成,需要再手动 profiler stop 输出结果。如果你在压测或者等问题复现,也可以不加duration,让它在后台一直采,等压测结束后手动执行 profiler stop。
采样过程中可以用 profiler getSamples 查看当前已经积累了多少个样本。如果这个数字一直不涨,说明采样事件可能没生效,需要检查event参数或JDK版本。
2.3 产出文件与浏览器查看
采样结束后执行:
code复制profiler stop
Arthas会生成SVG文件,并打印输出路径,通常是:
text复制profiler output file: /root/arthas-output/20250603-153000.svg
Arthas内置了一个HTTP服务,默认端口3658。直接在浏览器打开 http://localhost:3658/arthas-output/,就能看到所有生成的文件列表,点击即可查看火焰图。如果是远程SSH登录的机器,记得用隧道把端口转发回本地:
bash复制ssh -L 3658:localhost:3658 user@your-server
然后本地浏览器访问 http://localhost:3658/arthas-output/。这一步不需要把文件下载到本地,非常方便。
2.4 指定文件名和格式:stop参数细节
我通常会显式指定输出路径和格式,方便归档:
code复制profiler stop --file /data/logs/cpu.svg --format svg
--format 支持多种格式,最常用的是 svg 和 html。我个人更推荐html,因为html文件在浏览器里支持关键字搜索,点开一张几千帧的火焰图后能直接搜方法名,速度快很多。svg则更轻量,适合作为附件发到群里快速预览。
tree 格式也很有用,它会在终端直接输出文本调用树,适合没有图形界面、只能SSH的排查场景。但信息量比较密集,新手可以先从html看起。
常用参数整理成了一张表,方便对照:
| 参数 | 作用 | 我的建议 |
|---|---|---|
--event |
指定采样事件 | cpu/alloc/wall/lock,按问题类型选 |
--interval |
采样间隔 | 默认10ms,多数场景不用动 |
--duration |
采样时长 | 60秒够用,复杂问题120秒 |
--file |
输出文件路径 | 建议带应用名和时间戳 |
--format |
输出格式 | html优先,svg次之 |
--thread |
只采样指定线程 | 已定位到具体线程ID时使用 |
3. 不是只有CPU:alloc、wall、lock事件怎么选
很多教程写到 profiler start 就结束了,但你实际用几次会发现,选错event会让你白忙活一场。不同事件回答的是不同问题,下面逐个说清楚。
3.1 cpu事件:默认主力
cpu事件是默认值,回答的问题就是“CPU时间去哪了”。适合CPU使用率飙高、load飙高、线程池线程全忙的场景。它采样的是正在CPU上执行指令的线程栈,所以能看到真正的计算热点。
需要注意:如果服务CPU不高,但接口RT很高,cpu事件采出来的火焰图往往会比较“平”,因为线程大量时间不在CPU上,而是在等锁、等IO、等下游。这个时候CPU事件不能完整反映时间去向。
3.2 alloc事件:GC风暴的照妖镜
alloc事件采样的是对象分配热点。当你发现GC频繁、Full GC导致停顿,但cpu事件又看不出明显业务热点时,用alloc事件往往能找到答案。
采集命令:
code复制profiler start --event alloc
采样结束后生成的火焰图,会显示哪些方法栈分配了最多对象。我曾经用alloc事件定位过一个线上问题:一段日志打印逻辑用了字符串拼接而不是占位符,在高并发下每次请求都new出大量String和StringBuilder,直接推高了Young GC频率。从cpu火焰图上看,log相关的帧占比并不夸张,但alloc火焰图上,日志框架栈帧的宽度非常扎眼。
3.3 wall事件和lock事件:当问题不在CPU上
wall事件按墙钟时间采样,意思是“不管线程在CPU上跑还是在等待,只要它有调用栈就记录”。它最适合回答“时间到底耗在哪”这个问题,尤其是IO等待、sleep、锁等待造成的RT升高。
lock事件则针对性更强,专门采样锁竞争相关的调用栈。当你怀疑某个同步块或并发组件是瓶颈时,用lock事件能看到线程到底停在了哪个 Monitor.enter 或 park 上。
实际使用中我的选择逻辑很简单:
- CPU高 → cpu事件
- GC频繁、响应慢但CPU不高 → alloc事件
- RT高、线程大量BLOCKED/WAITING → wall事件
- 明显是锁竞争 → lock事件
3.4 按线程采样,缩小范围
如果已经通过 dashboard 或 thread -n 3 锁定了某个线程ID,可以在采样时只针对这个线程:
code复制profiler start --event wall --thread 232
这样生成的火焰图只包含指定线程的调用栈,排除干扰,定位更精准。尤其是几十个线程共用一张火焰图分不清谁是谁的时候,这个参数能救命。
4. 火焰图读图:从形状到代码行
图生成了,不等于问题解决了。读图是有方法论的,我见过不少人打开火焰图后第一眼直接懵掉,不知道从哪里看起。这里把读图逻辑拆开讲。
4.1 宽度、高度、颜色到底代表什么
火焰图里每个矩形代表一个方法调用栈帧,矩形越宽,说明这个栈帧在样本中出现的比例越高。x轴是样本占比,不是时间先后顺序;y轴是调用栈深度,在最顶部的是采样时正在执行的方法,在底部的是入口方法。
颜色是最容易被误解的信息。不同实现里颜色含义不一样,有的按类型分类,有的随机配色。判断依据永远是宽度,不是颜色。不要把红色帧当成“有问题”,也不要看到绿色就放松。
从阅读顺序上讲,先看最宽的顶部帧,那是真正消耗资源的位置,然后沿着它向下看调用链,找到入口代码。比如顶部很宽的是 com.example.service.OrderService.getOrders,向下看是 OrderDAO.query,再向下是 mysql-connector 的不少帧,基本就是SQL执行慢导致CPU或时间消耗在数据库驱动层。
4.2 三种值得警惕的常见形状
第一类是“平顶山”:某个栈帧特别宽,横跨大半个画面,下面只有一两层调用链。这通常是自循环、正则回溯、或者原生方法在疯狂消耗CPU。一旦看到顶部一条横带,先检查它名字里有没有 java.util.regex、Unsafe、xxhash这类关键字。
第二类是“深塔”:调用链很长,每一层都占了不少宽度。这说明时间均匀地消耗在整条调用链路上,往往需要做链路级的优化,比如减少不必要的中间层、合并服务调用。
第三类是“小草垛”:很多小栈,单个占比都不高,但加起来总量很大。常见于大量短生命周期任务并发执行,比如频繁创建线程、频繁执行小任务、大批量日志输出。这种场景需要看整体占比,别揪着某一个窄帧不放。
4.3 实例:一张正则回溯火焰图的判断链
我印象很深的一次排查:一个接口在某天高峰期突然从几十毫秒变成几秒,CPU上升但没到打满的程度。用 profiler start --event wall 采了一分钟,生成的火焰图顶部横着一条非常宽的帧,名字是 java.util.regex.Pattern$Curly.match,下方连着 String.replaceAll,再往下是一个订单备注校验方法。
这种结构一出来,问题几乎不需要猜了,就是正则回溯。后来定位到有一段 .* 配合贪婪量词的正则,用来匹配很长的商品备注字符串时发生了灾难性回溯。把它改成非贪婪写法,并给匹配长度加了上限,接口耗时直接降了一个数量级。
如果没有火焰图,这类问题靠jstack很难抓到,因为回溯发生期间线程一直在一个native层或正则内部方法里转,栈顶看到的可能只是 Pattern 相关方法,但很难形成直观的“宽度认知”。
4.4 高级技巧:故障态与正常态对比采样
单张火焰图只能说明热点长什么样,真正能给问题定性的,是对比。同一套服务,在故障高峰期采样一张,在低峰期或者修复后再采样一张,两张图放在一起看,多出来的那个“塔”,往往就是问题本身。
我习惯的做法是给文件命名带上场景标识:
code复制profiler stop --file /data/logs/cpu-bad-20250603-153000.html
故障修复后再采一张:
code复制profiler stop --file /data/logs/cpu-good-20250603-153000.html
然后两张图对照着看,新增的宽帧或明显变宽的部分,就是引入异常的代码路径。这个方法在版本发布后出现性能回退时特别好用,比翻代码diff还直观。
5. 生产环境能不能用Arthas火焰图:我的实战边界
“Arthas可以生产环境用吗”这个问题几乎每次分享都会被问到。我的答案是:可以,但你得清楚自己在做什么,以及哪些命令能碰、哪些命令不能轻易碰。
5.1 profiler在生产环境的风险评估
首先说结论:profiler命令是Arthas命令里侵入性相对最低的一类。它不改字节码、不加拦截器、不重启应用,只是在一段时间内通过采样拿调用栈。相比 watch、trace、tt 这类需要做字节码增强的命令,profiler对业务逻辑的影响小得多。
但有几个边界你必须清楚:
- attach本身需要目标JVM有权限,且attach那一瞬间还是会有一次短暂的JVM交互,在CPU已经打满的机器上,启动Arthas偶尔会慢,要有点耐心。
- 采样间隔默认10ms,对绝大多数服务影响可忽略。但如果你的服务本身已经满负荷,任何额外操作都可能产生轻微抖动,所以采样时长建议控制在60到120秒。
- Arthas会开启3658这个HTTP端口,一定不要暴露到公网。远程排查时用SSH隧道转发,别图省事直接开放防火墙。
- 退出Arthas用
stop命令,它会卸载agent并关闭服务端;只敲quit只是退出控制台,agent其实还挂在进程上。
5.2 一套可复制的生产排查节奏
我把平时在生产环境处理CPU问题的完整节奏整理成了一个固定流程,你照着走基本不会乱:
- 先用
dashboard看整体状态,确认CPU、内存、线程数的大盘,耗时几秒钟。 - 用
thread -n 3看当前最忙的线程,拿到可疑线程ID。 - 如果热点在线程栈上不够清晰,启动
profiler start --event cpu,用60秒采样。 - 在采样期间,尽量让问题复现,比如触发一次压测、跑一轮线上活动流量。
- 采样结束后执行
profiler stop --file /data/logs/cpu.html --format html,生成带时间戳的文件。 - 用SSH隧道把3658端口转发到本地,打开html文件读图。
- 定位到疑似热点后,再决定继续用
trace看方法内部耗时,还是直接翻代码。 - 问题确认后,执行
stop退出Arthas,确保agent卸载。
这套流程里,profiler负责的是从“现象很乱”到“定位代码路径”这一步,是承上启下的关键环节。
5.3 容易踩的坑和参数选择建议
坑一:采样完成后发现样本数特别少。这种情况多半是JDK版本或事件不支持,先确认你能用的event列表,执行 profiler list 查看当前支持的事件。
坑二:html文件太大导致浏览器卡。大业务量下采样两分钟,生成的火焰图动辄几十MB,直接打开会非常卡。解决办法是缩短采样时长,或者把 --interval 调到20ms、50ms,牺牲一点细节换取可用性。
坑三:忘记切event,用cpu事件查RT问题,折腾半天没有结论。CPU不高、接口慢的场景,从一开始就应该用wall事件。
坑四:采集完直接退出Arthas,没有先 profiler stop。这样不会生成文件,等于白采。我在不指定duration时习惯先设一个提醒,避免压测太投入忘了收尾。
参数选择上,我的默认组合是:定位CPU问题用 --event cpu --duration 60 --format html,定位RT问题用 --event wall --duration 60 --thread <tid>。两者有重叠,但在大多数场景下足够覆盖需求。调采样间隔只有在需要更长采样时间但又不想生成超大文件时才需要,日常排查很少动它。
最后分享一个我自己的小习惯:每次用Arthas采完火焰图,我都会把故障时和正常时的两张图归档在一起,文件名带上应用版本和时间戳。线上性能问题最怕事后只能靠回忆复盘,有图、有对比、有上下文,处理问题时能少走很多弯路。
