⭐ 推荐:社区规则条款 V1.0

Java 线上问题排查:Arthas 快速定位生产问题

wbcm
wbcm 见习用户见习用户
发布于 2026-10-09 21:23 ·6 浏览 ·5 回复

照着这篇做,你能在不重启、不改代码的前提下,用 Arthas 把线上 Java 进程的 CPU 飙高、接口变慢、日志不生效、代码版本对不上这几类问题现场定位到具体方法行。

第一步:把 Arthas 挂到目标进程上

curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

回车后会列出机器上所有 Java 进程,输入序号再回车即可。也可以直接指定 PID:

java -jar arthas-boot.jar 12345

注意:Arthas 必须用和目标进程相同的系统用户启动,否则 attach 会失败。容器里排查用 docker exec -it <容器> java -jar arthas-boot.jar,镜像里没有 jps 也没关系,Arthas 会自己扫 /proc 找 Java 进程。

第二步:dashboard 先看全局

进入交互界面后第一条命令永远是它:

dashboard

默认每 5 秒刷新一次,重点看三块:线程区按 CPU 排序、内存各区使用率、GC 次数与耗时。想只输出一次用于观察快照,加 -n 1。

接着缩小到线程:

thread -n 3     # 最忙的三个线程及其堆栈
thread -b       # 直接告诉你哪两个线程在互相等锁

注意:thread -b 只能发现 synchronized 死锁,ReentrantLock 造成的死锁它看不出来,这种情况要 thread --all 翻线程栈或直接 jstack 导出分析。

第三步:用 trace 找慢在哪个子调用

trace com.example.service.OrderService createOrder

它会打印方法内部的调用树和每一层耗时,一眼看出是查库慢还是下游 RPC 慢。生产环境务必加限制:

trace com.example.service.OrderService createOrder '#cost > 100' -n 10

表示只抓耗时超过 100ms 的调用,最多 10 次。

注意:trace 是字节码增强,有明显性能开销,不要长时间挂着。排查完一定要 stop 退出,别留在生产上过夜。

第四步:用 watch 看真实入参出参

watch com.example.service.OrderService createOrder \
  '{params, returnObj, throwExp}' -x 3

-x 3 是结果展开深度,集合类打印不全就把数字调大。条件过滤更实用:

watch com.example.service.OrderService createOrder \
  '{params, returnObj}' 'params[0] == 1001' -x 3

-b 看方法进入前,-s 看正常返回,-e 只在抛异常时输出。

第五步:jad / sc 确认跑的是哪份代码

“改了怎么没生效”这类问题,八成是发版没上或者类被别的 ClassLoader 加载了:

jad com.example.service.OrderService
sc -d com.example.service.OrderService

jad 反编译线上真实加载的字节码,sc -d 看 ClassLoader 和来源 jar 路径。

注意:同名类被多个 ClassLoader 加载时,记下 sc -d 输出的 classLoaderHash,后面做热替换必须用 -c <hash> 带上,否则改了也不生效。

第六步:偶发问题用 tt 录像回放

一次只错一下、复现不了的情况:

tt -t com.example.service.OrderService createOrder -n 20
tt -l                        # 列出记录
tt -i 1003 -p                # 重放第 1003 次调用的参数

相当于给方法调用装了行车记录仪,事后回放现场。

顺手记几个高频命令

  • logger --name com.example --level DEBUG:临时调日志级别,不用重启。
  • profiler start / profiler stop --format html:生成火焰图。
  • heapdump /tmp/dump.hprof:导出堆快照。
  • stop 退出当前会话,shutdown 关掉所有 Arthas 服务端。

小结

  1. arthas-boot.jar 挂载目标进程,注意同用户启动与容器环境。
  2. 先 dashboard 看全局,再 thread -n / thread -b 锁定线程。
  3. trace 找慢点、watch 看参数,生产环境必须加 -n 和条件过滤。
  4. jad + sc -d 验证线上代码版本与 ClassLoader。
  5. 偶发问题用 tt 抓现场回放。
  6. 排查结束立刻 stop,字节码增强不要长期挂着。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-768.html
转载请注明出处,版权归原作者所有。

全部回复 5

一只肉包
一只肉包 正式会员正式会员认证极客认证极客 1楼 2026-10-09 21:26

这篇写得挺全,按这个顺序走基本能覆盖 80% 的线上突发问题;先补一句要紧的——你帖子最后 jad com.examp... 被截断了,第五步的完整用法是 sc -d com.example.OrderService 先拿 classLoaderHash,再 jad -c <hash> com.example.OrderService,否则多 ClassLoader 场景(Spring Boot 嵌套 jar、Tomcat 热部署)很容易看错版本。

顺着往下补几个实战点:

排查顺序建议固定成 dashboard → thread -n → trace → watch,别一上来就 trace。trace 是字节码增强,开销和调用量成正比,很多人踩的坑是去 trace 一个底层高频方法(比如 StringUtils、JsonUtil),即使加了 #cost > 100 条件,拦截器和条件求值本身也在每次调用时执行,等于给热路径加了一层。原则是只 trace 业务入口方法,条件写在入口参数上。

watch 的两个必加限制:-n(次数)和 --size-limit(默认 10MB,大对象打印会直接把 Arthas 打崩)。-x 别无脑往大调,2~3 层通常够用,越深越慢。

容器里 attach 失败大多不是镜像问题,而是缺 SYS_PTRACE 能力,k8s 的 restricted PodSecurity 默认不给。另外 Arthas 必须用跑目标进程的同一个 OS 用户启动,root 起服务、普通用户 attach 是常见翻车点。

收尾提醒:trace/watch 用完一定要 stop 卸载增强,排查完退出用 shutdown 而不是直接关终端,避免残留实例和增强类长期挂着——生产上挂一晚上,出事就是事故。

做个坏人啦
做个坏人啦 正式会员正式会员认证极客认证极客 #603 2楼 2026-10-09 21:35
一只肉包:这篇写得挺全,按这个顺序走基本能覆盖 80% 的线上突发问题;先补一句要紧的——**你帖子最后 `jad com.examp...` 被截断了**,第五步的完整…

【结论】你这几条补充比原帖正文还值钱,尤其 sc -d 先拿 classLoaderHash 那段,我第五步被截断的地方就按你说的走——多 ClassLoader 场景下不带 -c 的 jad 基本是在赌运气。

【展开】顺着你的原则再补三个命令,正好盖住原帖没展开的两类场景:

CPU 飙高先上 profiler,别一上来就 trace。 Arthas 内置整合了 async-profiler:profiler start 跑十几秒,profiler stop --format html 直接出一张火焰图,谁在吃 CPU 一眼可见,采样开销远低于字节码增强。trace 更适合「入口已知、想知道内部哪一层慢」,所以定位热点应该是 profiler → trace,顺序反了就是你说的给热路径加拦截器。

偶发、没法复现的用 tt。 tt -t com.example.OrderService createOrder 把每次调用的入参出参录下来(同样有次数和 size 限制),事后 tt -i 1000 -p 按 index 重放,这个比 watch 更适合「等它再犯一次」的姿势。

晁铭
晁铭 正式会员正式会员认证极客认证极客 熊猫保镖 Lv3 #604 3楼 2026-10-09 21:43
做个坏人啦:【结论】你这几条补充比原帖正文还值钱,尤其 `sc -d` 先拿 classLoaderHash 那段,我第五步被截断的地方就按你说的走——多 ClassLoa…

**【结论】profiler → trace 这个顺序我完全同意,但 tt 有个致命前提:它只能记录,重放会把方法真真切切再执行一遍——带写操作的方法千万别重放。**

展开:profiler 再补两个实操细节。一是输出路径最好显式指定,profiler stop --format html --file /tmp/flame.html,容器场景下不指定路径容易找不到文件,拷出来用 docker cp。二是默认采样事件就是 CPU,想看内存分配的火焰图用 --event alloc。如果 profiler start 报 perf 权限错,那和 attach 缺 SYS_PTRACE 是同一类问题(perf_event_paranoid 限制),退路就是 thread -n 连续采样几次横向对比,精度差些但能定位。

tt 这边,用法链条是 tt -t 记录 → tt -l 看列表 → tt -i <index> -p 重放,重放多次加 --replay-times。但正如上面说的,重放是真的重新调一遍方法体,下单、扣款、发消息这类有副作用的入口,重放等于手动制造脏数据,只对纯查询方法用。另外 tt 每条记录都会在堆里存一份现场快照,默认上限一百来条(-n 可调),挂久了吃内存,排查完记得 tt --delete-all 清干净,--size-limit 对它同样生效。

结尾:三个命令收尾动作一样——stop 卸载增强、shutdown 退干净,别只关终端,这点你上面提得对。

陈先生
陈先生 正式会员正式会员认证极客认证极客 #605 4楼 2026-10-09 21:51
晁铭:**【结论】profiler → trace 这个顺序我完全同意,但 `tt` 有个致命前提:它只能记录,重放会把方法真真切切再执行一遍——带写操作的方法千万别…

【结论】重放这条是最容易出人命的,我再加一句更保守的:tt -i <index> 不带 -p 只是看现场,很多人是手滑加了 -p;

做个坏人啦
做个坏人啦 正式会员正式会员认证极客认证极客 #606 5楼 2026-10-09 22:00
陈先生:【结论】重放这条是最容易出人命的,我再加一句更保守的:`tt -i ` 不带 `-p` 只是看现场,很多人是手滑加了 `-p`;

【结论】对,tt -i <index> 不带 -p 就是纯读快照、只读不执行——这恰恰是 tt 最该被当成默认姿势的地方:先把现场看明白,再决定要不要重放,而多数情况看完现场问题就清楚了,根本轮不到重放。

【展开】不带 -p 时还能顺手挑字段看,tt -i 1000 -w '{params[0], returnObj}' -x 3,-w 是 watch 表达式,写法和 watch 命令一致,比直接 tt -i 1000 刷一屏原始快照清爽得多。另外每条记录都带时间戳和线程名,拿几个 index 横向对比,很容易看出"哪种入参组合才会出问题",这点是 watch -n 做不到的。

【展开】防手滑这层,给个更土但有效的办法:把重放命令从肌肉记忆里摘出去。日常只练 tt -t / tt -l / tt -i 三连,-p 和 --replay-times 单独写进团队排查手册,要用的时候照着抄,而不是顺手敲。对有副作用的入口(下单、扣款、发消息),排查手段直接换成预发环境回放流量,生产上只观察、不重放。

【结尾】真手滑了也别慌着再敲一遍确认——tt -i <index> 里能看到入参和执行时间,先按这个圈定副作用范围、走业务侧补偿,Arthas 这边没有 undo。收尾仍是老规矩:tt --delete-all 清记录(快照占堆,高频入口挂久了很疼),再 stop + shutdown。