Arthas线上故障排查实战手册

Cosolar 3 阅读 后端

本手册面向 Java 后端开发与运维工程师,系统讲解如何使用 Alibaba Arthas 诊断线上问题。每个场景包含问题现象、排查思路、具体命令和解决方案,可作为线上排障的案头参考。

一、Arthas 简介

Arthas 是 Alibaba 开源的 Java 线上诊断工具,能够在不修改应用代码、不重启 JVM 的前提下,实时查看应用的线程状态、方法调用出入参、类加载信息、JVM 运行指标等,并支持动态修改运行时行为。

解决了什么痛点

生产环境出了问题,传统排查方式存在三大困境:

  1. 无法调试——生产环境不允许挂断点调试,因为 suspend 所有线程会导致服务停顿。
  2. 难以复现——有些问题在测试环境无法复现,重启 JVM 后问题可能消失。
  3. 加日志成本高——修改代码→测试→预发→生产,整个流程耗时数小时,且问题可能已经不复现。

Arthas 通过字节码增强技术,以"观察者"身份注入运行中的 JVM,不暂停线程,不动业务代码,即可完成方法级观测与诊断。

核心能力一览

能力分类 对应命令 典型用途
全局监控 dashboard 实时查看线程、内存、GC 概览
线程诊断 thread 定位 CPU 高占用线程、死锁检测
方法观测 watch 查看方法入参、返回值、异常
调用链追踪 trace 定位方法内部耗时瓶颈
调用路径 stack 查看方法被谁调用
方法监控 monitor 统计方法调用次数、成功率、平均 RT
时空隧道 tt 记录方法历史调用,事后回放
类反编译 jad 确认运行中加载的类是否为预期版本
类搜索 sc / sm 查找已加载的类与方法
字段查看 getstatic / vmtool 直接读取静态字段或堆内对象
动态执行 ognl 执行任意表达式,调用任意方法
内存编译 mc 编译 .java 为 .class
热更新 redefine / retransform 运行时替换字节码
性能分析 profiler 生成火焰图
日志管理 logger 动态修改日志级别

版本与环境要求

Arthas 4.x 支持 JDK 8+,包括 JDK 17、JDK 21 和 JDK 25,覆盖 Linux/Mac/Windows 平台。采用命令行交互模式,支持 Tab 自动补全,同时支持 telnet 和 WebSocket 连接方式(本地与远程均可)。

二、安装与快速启动

2.1 快速安装

# 下载 arthas-boot.jar
curl -O https://arthas.aliyun.com/arthas-boot.jar

# 启动(会列出当前机器上的 Java 进程,选择要诊断的进程)
java -jar arthas-boot.jar

启动后会看到类似下面的进程列表:

[INFO] arthas-boot version: 4.x.x
[INFO] Found existing java process, please choose one and input the serial number:
* [1]: 12345 com.example.MyApplication
  [2]: 23456 org.apache.catalina.startup.Bootstrap

输入序号即可 attach 到目标进程。

2.2 指定进程启动

如果已知进程 PID 或名称,可以直接指定,跳过选择步骤:

# 通过 PID 指定
java -jar arthas-boot.jar 12345

# 通过进程名指定(推荐,因为 PID 每次重启会变)
java -jar arthas-boot.jar --select MyApplication

2.3 Docker 环境中使用

容器环境下,Arthas 需要进入目标容器的命名空间:

# 方式一:进入容器后执行
docker exec -it <container_id> /bin/bash
java -jar arthas-boot.jar

# 方式二:使用 docker exec 直接运行(需容器内已有 JDK)
docker exec -it <container_id> java -jar /path/to/arthas-boot.jar

对于精简镜像(如基于 Alpine 且无 JDK 的镜像),可以将 Arthas 的 agent jar 挂载进去,或使用 arthas-spring-boot-starter 随应用一起打包。

2.4 通过 Web Console 远程连接

Arthas 启动后会同时监听本地 telnet(端口 3658)和 HTTP(端口 8563)。通过浏览器访问 http://<host>:8563 即可使用 Web Console,适合远程排查无法 SSH 的环境。

对于多服务器场景,可以部署 Arthas Tunnel Server 统一管理多个节点上的 Arthas 连接。

2.5 随应用启动(Spring Boot Starter)

pom.xml 中引入依赖,Arthas 会随应用自动启动:

<dependency>
    <groupId>com.taobao.arthas</groupId>
    <artifactId>arthas-spring-boot-starter</artifactId>
    <version>4.x.x</version>
    <scope>provided</scope>
</dependency>

建议设为 provided 或配合配置开关控制,避免在生产镜像中默认开启。

2.6 退出与卸载

# 退出当前 Arthas 会话(不影响应用运行)
quit
# 或
exit

# 彻底卸载 Arthas(清理增强的字节码)
stop

stop 命令会移除 Arthas 对 JVM 的所有增强,恢复原始字节码。生产排查完毕后建议执行 stop 确保环境干净。

三、核心命令体系总览

3.1 诊断流程方法论

面对线上问题,建议遵循"先全局、后局部,先现象、后根因"的排查路径:

dashboard(全局概览)
    │
    ├─ CPU 高 → thread -n <N>(找最忙线程)→ thread <id>(看堆栈)→ jad(反编译确认)→ trace/watch(定位具体方法)
    │
    ├─ 接口慢 → trace(追踪耗时链路)→ watch(观测入参出参)→ 找到瓶颈节点
    │
    ├─ 异常报错 → watch(观测异常)→ stack(看调用来源)→ jad(确认代码逻辑)
    │
    ├─ 内存/OOM → dashboard(看内存区域)→ vmtool(查堆内对象)→ heapdump(导出堆)
    │
    └─ 类加载问题 → sc(查类是否加载)→ jad(反编译看版本)→ dump(导出字节码比对)

3.2 命令分类速览

JVM 诊断类:

命令 作用 使用频率
dashboard 实时仪表盘,展示线程、内存、GC、运行环境 极高
thread 查看线程信息,定位 CPU 高占用、死锁 极高
jvm 查看 JVM 详细信息(版本、启动参数、类加载器)
memory 查看各内存区域使用情况
sysprop 查看/修改系统属性
sysenv 查看环境变量
vmoption 查看/修改 JVM 诊断参数
getstatic 查看类的静态字段
vmtool 从堆中查询指定类的实例对象
heapdump 导出堆 dump 文件

方法诊断类:

命令 作用 使用频率
watch 观察方法入参、返回值、异常,支持条件过滤 极高
trace 追踪方法内部调用链路及各节点耗时 极高
stack 输出方法被调用的调用栈
monitor 统计方法调用次数、成功率、平均 RT
tt 时空隧道,记录并回放方法历史调用

类与字节码类:

命令 作用 使用频率
sc 查看已加载的类信息
sm 查看已加载类的方法信息
jad 反编译指定类,查看实际运行的代码 极高
mc 内存编译 .java 文件为 .class
redefine 热加载 .class 文件替换运行时类
retransform 批量 retransform 已加载的类
dump 导出已加载类的字节码到文件
classloader 查看类加载器信息

其他工具类:

命令 作用 使用频率
ognl 执行 OGNL 表达式,调用任意方法
logger 查看/修改日志级别
profiler 生成火焰图,性能分析
options 全局选项配置

四、实战场景详解

场景一:CPU 飙高排查

问题现象

线上监控告警,某台服务器 CPU 使用率持续 95%+,但接口 QPS 并未明显增长。应用表现为整体卡顿,部分接口超时。

排查思路

CPU 飙高通常有两类原因:一是某个或某些线程在执行 CPU 密集型计算(如死循环、正则回溯、序列化大对象),二是频繁 Full GC 导致。需要先判断是哪一类,再定位到具体线程和代码。

详细步骤

第一步:全局概览

[arthas@12345]$ dashboard

重点关注两个区域:

  • Threads 区域:观察哪些线程 CPU 占比高,状态是否为 RUNNABLE。
  • Memory / GC 区域:观察是否频繁 Full GC。如果 Full GC 次数快速增长,说明可能是内存问题导致 CPU 飙高。

第二步:定位最忙线程

# 查看占用 CPU 最高的 3 个线程
[arthas@12345]$ thread -n 3

输出示例:

threads name                  cpu      state
pool-1-thread-1               85.6%    RUNNABLE
http-nio-8080-exec-3          8.2%     RUNNABLE
GC thread                     4.1%     RUNNABLE

pool-1-thread-1 的 CPU 占用高达 85.6%,这就是问题线程。

第三步:查看线程堆栈

# 查看指定线程的完整堆栈
[arthas@12345]$ thread 1

输出示例:

"pool-1-thread-1" Id=32 cpu=85.6% RUNNABLE
    at com.example.service.OrderService.calculatePrice(OrderService.java:128)
    at com.example.service.OrderService.processOrder(OrderService.java:95)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)

堆栈显示卡在 OrderService.calculatePrice 方法的第 128 行。

第四步:反编译确认代码

# 反编译确认运行中的代码逻辑
[arthas@12345]$ jad com.example.service.OrderService calculatePrice

输出会显示反编译后的源码,检查第 128 行附近的逻辑。如果发现是 while 循环条件判断有误导致死循环,就找到了根因。

第五步:追踪方法耗时(确认是否死循环)

# 追踪该方法内部调用,观察是否一直在循环
[arthas@12345]$ trace com.example.service.OrderService calculatePrice -n 5 --skipJDKMethod false

如果 trace 结果显示该方法执行时间极长且持续不返回,基本确认是死循环。

解决方案

  • 临时止血:通过 thread -b 检查是否有阻塞线程,必要时在代码中增加循环退出条件或超时机制。如果该线程是非关键业务线程,可以考虑通过 thread 暂停(不建议在生产直接操作)。
  • 根因修复:修复死循环逻辑,重新发布。
  • 如果是 Full GC 导致:转至场景四:内存泄漏与 OOM 排查

常见 CPU 飙高根因清单

根因 特征 确认方式
死循环 单线程 CPU 持续 90%+,方法长时间不返回 thread -n + trace
正则回溯 CPU 飙高但无明显业务异常,涉及正则匹配 thread 堆栈可见 java.util.regex
序列化大对象 JSON/toString 循环引用或对象极深 thread 堆栈可见 com.fasterxml.jacksontoString
频繁 Full GC GC 线程 CPU 高,dashboard 中 Full GC 次数快速增长 dashboard 内存区域
加密/压缩 涉及 MessageDigestDeflater 等 CPU 密集操作 thread 堆栈可见相关类

场景二:接口响应缓慢排查

问题现象

用户反馈订单查询接口从正常的 100ms 变慢到 3 秒以上,但 CPU 和内存使用率正常,无明显错误日志。

排查思路

接口慢的原因可能出在方法内部调用的任何一个环节:数据库查询、远程 RPC 调用、序列化、锁竞争等。需要用 trace 逐层追踪调用链,找到耗时最长的节点。

详细步骤

第一步:确定入口方法

假设慢接口对应的 Controller 方法为 com.example.controller.OrderController.queryOrder

第二步:追踪调用链耗时

[arthas@12345]$ trace com.example.controller.OrderController queryOrder -n 5

输出示例:

`---[3205ms] com.example.controller.OrderController:queryOrder()
    +---[0.15ms] com.example.dto.OrderRequest:validate()
    +---[3012ms] com.example.service.OrderService:getOrder()
    |   `---[2980ms] com.example.mapper.OrderMapper:selectById()
    |       `---[2975ms] org.apache.ibatis.session.SqlSession:selectOne()
    +---[180ms] com.example.service.PriceService:calculatePrice()
    `---[12ms] com.example.converter.OrderConverter:toVO()

一目了然:OrderMapper.selectById 耗时 2980ms,占总耗时的 93%。问题出在数据库查询。

第三步:观测入参(确认是否为特定参数导致)

# 观察方法入参,展开 2 层
[arthas@12345]$ watch com.example.mapper.OrderMapper selectById "{params, returnObj}" -x 2

触发一次请求后可以看到:

method=com.example.mapper.OrderMapper.selectById params=[null]

入参为 null,说明上游传入的订单 ID 为空,可能导致数据库全表扫描。

第四步:进一步追踪上游

# 追踪 OrderService 中传入 mapper 的值来源
[arthas@12345]$ trace com.example.service.OrderService getOrder -n 3

可能发现是从缓存中取到的订单 ID 为 null,但缓存逻辑未做空值判断,直接透传给了数据库查询。

进阶:条件过滤追踪

当接口调用频繁,只想看慢于某个阈值的调用:

# 只追踪耗时超过 500ms 的调用
[arthas@12345]$ trace com.example.controller.OrderController queryOrder '#cost > 500' -n 5

#cost 是 Arthas 内置变量,表示方法执行耗时(毫秒)。

解决方案

瓶颈位置 解决方案
数据库查询慢 检查索引、SQL 执行计划;考虑加缓存
远程 RPC 调用慢 检查下游服务状态、网络延迟;增加超时与熔断
序列化耗时 检查是否有循环引用、大对象;优化序列化方式
锁竞争 通过 thread -b 查看阻塞线程;优化锁粒度
日志同步写盘 改为异步日志;检查日志级别是否过低

trace 命令关键参数说明

trace <class-pattern> <method-pattern> [条件表达式] [参数]

#cost          方法执行耗时(毫秒),可用于条件过滤
-n <N>         最多匹配次数,达到后自动停止(重要,避免持续增强)
--skipJDKMethod <true|false>  是否跳过 JDK 方法(默认 true)
-x <N>         展开层级(对 trace 不常用,主要用于 watch)
-v             打印条件表达式的执行结果

场景三:方法异常与空指针定位

问题现象

线上偶发 NullPointerException,但日志中没有完整的调用链路,无法确定是哪个方法返回了 null。错误日志只显示了一行 at com.example.service.CouponService.apply(CouponService.java:76),看不到上游传入的参数。

排查思路

需要用 watch 命令观测方法的入参、返回值和抛出的异常,找到导致 NPE 的具体数据。如果问题偶发,还需要结合条件过滤来精准捕获。

详细步骤

第一步:观测方法入参和异常

# 观测入参和抛出的异常,展开 3 层
[arthas@12345]$ watch com.example.service.CouponService apply "{params, throwExp}" -x 3

触发业务请求后输出:

method=com.example.service.CouponService.apply params=[CouponRequest(userId=12345, couponId=null, amount=100.0)]
throwExp=java.lang.NullPointerException: Cannot invoke "Coupon.getType()" because "coupon" is null

关键信息:couponId 为 null,导致后续查询优惠券返回 null,调用 coupon.getType() 时 NPE。

第二步:只观测抛出异常的调用

如果方法正常调用居多,异常只是偶发,可以加条件过滤:

# 只在抛出异常时输出
[arthas@12345]$ watch com.example.service.CouponService apply "{params, throwExp}" "#throwExp != null" -x 3

#throwExp 是内置变量,表示方法抛出的异常对象。条件表达式 #throwExp != null 确保只在异常发生时才输出。

第三步:查看调用来源

# 查看是谁调用了 apply 方法
[arthas@12345]$ stack com.example.service.CouponService apply -n 3

输出示例:

ts=2026-07-15 10:30:45; thread_name=http-nio-8080-exec-5; id=45; is_daemon=false; priority=5;
    @com.example.controller.OrderController.createOrder()
        at com.example.service.OrderService.processOrder(OrderService.java:89)
        at com.example.service.CouponService.apply(CouponService.java:70)

确认调用链是 OrderController.createOrder → OrderService.processOrder → CouponService.apply

第四步:反编译确认代码逻辑

[arthas@12345]$ jad com.example.service.CouponService apply

检查反编译后的代码,确认第 70-76 行的逻辑——可能是查询优惠券后未做 null 检查就直接调用方法。

解决方案

  • 立即修复:在 CouponService.apply 中增加空值检查,优惠券不存在时返回友好提示而非 NPE。
  • 防御性编程:上游 OrderService 在传入 couponId 前应校验非空。
  • 临时热修复:如果无法立即重新发布,可参考场景八:热更新代码紧急修复

watch 命令核心参数

watch <class-pattern> <method-pattern> <观察表达式> [条件表达式] [参数]

观察表达式中可用的内置变量:
  params       方法入参数组
  returnObj    方法返回值
  throwExp     方法抛出的异常
  target       方法所在对象实例
  #cost        方法执行耗时(ms)

常用参数:
  -x <N>       展开层级,-x 1 只显示一层,-x 3 展开三层嵌套对象
  -n <N>       匹配次数上限
  -b           在方法调用前观察(此时 returnObj 和 throwExp 为 null)
  -e           在方法抛出异常后观察
  -s           在方法返回后观察
  -f           在方法结束后观察(无论正常返回或异常,默认行为)

场景四:内存泄漏与 OOM 排查

问题现象

应用每隔 2-3 天就会 OOM 重启,重启后恢复正常。GC 日志显示 Old 区持续增长,Full GC 后内存回收不明显。

排查思路

内存泄漏的典型特征是:对象被持续创建但无法被 GC 回收。需要先确认是哪个内存区域泄漏,再找到泄漏的具体对象类型,最后定位到持有这些对象的引用链。

详细步骤

第一步:查看内存区域使用情况

[arthas@12345]$ memory

输出示例:

Memory used     total      max      usage
heap   1.2G     2.0G       4.0G     30.42%
non_heap 256M   320M       -1       -
g1_eden 120M    160M       -1       75.00%
g1_old  1.0G    1.6G       4.0G     63.13%
g1_survivor 32M 48M        -1       66.67%

g1_old 区使用率 63%,如果这个值持续增长且 Full GC 后不下降,说明老年代有对象无法回收。

第二步:通过 dashboard 观察 GC 趋势

[arthas@12345]$ dashboard

观察 Memory 区域的 GC 次数和耗时趋势。如果 Full GC 频率越来越高但 Old 区不降,确认是内存泄漏。

第三步:查询堆中对象实例

# 查询堆中某个类的实例对象,限制 10 个
[arthas@12345]$ vmtool --action getInstances --className com.example.model.OrderCache --limit 10

输出会显示找到的实例及其内容。如果发现 OrderCache 实例数量异常多(比如几万个),说明这些对象无法被回收。

第四步:查看对象引用链(关键)

# 查看实例被谁引用(找到 GC Root 引用链)
[arthas@12345]$ vmtool --action getInstances --className com.example.model.OrderCache --limit 1 --express 'instances[0]'

进一步可以查看持有这些对象引用的字段:

# 查询某个缓存类的实例
[arthas@12345]$ vmtool --action getInstances --className java.util.concurrent.ConcurrentHashMap --limit 1 \
  --express 'instances[0].size'

如果发现某个 ConcurrentHashMap 的 size 达到了几十万,且 key 是不断增长的订单数据,而该 Map 没有清理机制,就是泄漏源头。

第五步:导出堆 dump 做离线分析

# 导出完整堆 dump
[arthas@12345]$ heapdump /tmp/heapdump.hprof

将 dump 文件下载到本地,用 MAT (Memory Analyzer Tool) 或 JProfiler 进行离线分析,查看 Dominator Tree 和 GC Root 引用链,精确定位泄漏对象。

第六步:查看静态字段确认缓存类

# 查看某个缓存管理类的静态字段
[arthas@12345]$ getstatic com.example.cache.OrderCacheManager cacheMap

或使用 vmtool 查询缓存管理器实例并查看其内部 Map 的大小。

解决方案

泄漏场景 典型表现 解决方案
缓存无上限 Map/List 持续增长 使用 Caffeine/Guava Cache 设置最大容量和过期策略
ThreadLocal 未清理 线程池中 ThreadLocal 数据残留 在 finally 中调用 ThreadLocal.remove()
监听器未注销 事件监听器持续累积 在对象销毁时注销监听器
静态集合引用 静态 Map 持有对象引用 避免静态集合持有业务对象;定期清理
数据库连接未关闭 连接池耗尽 + 连接对象泄漏 使用 try-with-resources;检查连接归还逻辑

内存排查命令对比

命令 适用场景 优势
dashboard 快速判断哪个内存区域异常 全局概览,一目了然
memory 精确查看各内存区域数值 数据准确,按区域细分
vmtool 查询堆中特定类的实例 在线分析,无需导出 dump
getstatic 查看静态字段值 直接读取,适合缓存类排查
heapdump 导出完整堆做离线分析 可用 MAT 做深度引用链分析

场景五:动态修改日志级别

问题现象

线上某个接口偶发异常,但当前日志级别为 INFO,缺少 DEBUG 级别的详细日志。重新发布修改日志级别成本太高且会导致问题可能无法复现。

排查思路

Arthas 的 logger 命令可以在运行时动态修改日志级别,无需重启应用。这是线上排障最高频的操作之一。

详细步骤

第一步:查看当前日志框架和级别

[arthas@12345]$ logger

输出示例:

 name                                   level  classLoader    classLoaderHash
 ROOT                                   INFO   BootClassLoader 5a54a66
 com.example                            INFO   BootClassLoader 5a54a66
 com.example.service.OrderService       INFO   BootClassLoader 5a54a66
 org.springframework                    WARN   BootClassLoader 5a54a66
 org.mybatis                            DEBUG  BootClassLoader 5a54a66

可以看到 com.example 的日志级别是 INFO。

第二步:修改日志级别为 DEBUG

# 修改指定包的日志级别
[arthas@12345]$ logger --name com.example --level DEBUG

修改后,com.example 包下所有类的 DEBUG 日志会立即开始输出。

第三步:针对特定类修改

# 只对某个类开启 DEBUG
[arthas@12345]$ logger --name com.example.service.OrderService --level DEBUG

第四步:排查完毕后恢复

# 恢复为 INFO
[arthas@12345]$ logger --name com.example --level INFO

注意事项

  • logger 命令支持 Logback、Log4j、Log4j2 三种主流日志框架,会自动识别。
  • 如果应用使用了自定义的日志配置(如编程式配置),可能无法通过 logger 命令修改,此时可以用 ognl 直接调用日志框架的 API:
# 通过 OGNL 直接调用 Logback API 修改级别
[arthas@12345]$ ognl '@org.slf4j.LoggerFactory@getLogger("com.example").setLevel(@ch.qos.logback.classic.Level@DEBUG)'

场景六:类冲突与类加载问题排查

问题现象

应用启动正常,但运行时抛出 NoSuchMethodErrorClassCastException。明明本地代码没问题,线上却报错。怀疑是不同版本的 jar 包冲突,加载了错误版本的类。

排查思路

类冲突问题的核心是确认 JVM 实际加载的是哪个版本的类、来自哪个 jar 包。需要用 sc 查看类加载信息,用 jad 反编译确认代码版本,用 dump 导出字节码做比对。

详细步骤

第一步:查找已加载的类

# 查找所有匹配的已加载类(支持通配符)
[arthas@12345]$ sc -d com.example.dto.OrderRequest

输出示例:

 class-info        com.example.dto.OrderRequest
 code-source       /data/app/lib/example-dto-1.2.0.jar
 name              com.example.dto.OrderRequest
 isInterface       false
 isAnnotation      false
 isEnum            false
 isAnonymousClass  false
 isArray           false
 isLocalClass      false
 isMemberClass     false
 isPrimitive       false
 isSynthetic       false
 simple-name       OrderRequest
 modifier          public
 annotation        
 interfaces        
 super-class       java.lang.Object
 class-loader      +-sun.misc.Launcher$AppClassLoader@5a54a66
                     +-sun.misc.Launcher$ExtClassLoader@1234abcd
 classLoaderHash   5a54a66

关键信息是 code-sourceexample-dto-1.2.0.jar。如果预期应该是 1.3.0 版本,说明打包时引入了旧版本。

第二步:检查是否存在多个版本

# 查看所有匹配的类(可能存在多个版本被不同 ClassLoader 加载)
[arthas@12345]$ sc -d *OrderRequest*

如果看到两条记录来自不同的 jar 包版本,确认是版本冲突。

第三步:反编译确认代码差异

# 反编译查看实际运行的代码
[arthas@12345]$ jad com.example.dto.OrderRequest

检查反编译后的代码是否包含预期的新方法。如果旧版本缺少某个方法,就会导致 NoSuchMethodError

第四步:导出字节码做比对

# 导出运行中的字节码到文件
[arthas@12345]$ dump com.example.dto.OrderRequest -d /tmp/dump

导出后可以用 javap -p 查看字节码,或与预期版本的 jar 包做 diff 比对。

第五步:查看类加载器树(复杂场景)

# 查看所有类加载器
[arthas@12345]$ classloader

输出示例:

 name                                       loadedCount  hash        parent
 BootstrapClassLoader                       2845         null        null
 sun.misc.Launcher$ExtClassLoader           12           1234abcd    BootstrapClassLoader
 sun.misc.Launcher$AppClassLoader           3567         5a54a66     ExtClassLoader
 org.springframework.boot.loader.LaunchedURLClassLoader  2341  abcdef12  AppClassLoader

在 Spring Boot fat jar 部署场景下,类加载器层级较深,需要确认类是从哪个加载器加载的。

解决方案

  • Maven 依赖排查:使用 mvn dependency:tree | grep <artifactId> 找到传递依赖的旧版本,用 <exclusion> 排除。
  • 强制指定版本:在 pom.xml<dependencyManagement> 中锁定正确版本。
  • 清理 lib 目录:如果是 lib 目录下存在多个版本的 jar,删除旧版本。
  • 类加载器隔离:如果业务上确实需要多版本共存(如插件化架构),使用自定义 ClassLoader 做隔离。

场景七:线程死锁排查

问题现象

应用某个功能完全卡住,接口请求超时但无异常日志。CPU 使用率很低(说明线程都在等待而非计算),但功能不可用。

排查思路

死锁的典型特征是多个线程互相等待对方持有的锁,导致全部阻塞。Arthas 的 thread 命令可以一键检测死锁。

详细步骤

第一步:检测死锁

# 检测死锁(Arthas 会自动分析线程的锁等待关系)
[arthas@12345]$ thread -b

-b 参数会找出阻塞其他线程的线程。如果有死锁,会输出死锁环中的线程信息。

第二步:查看阻塞线程详情

# 查看指定线程的堆栈
[arthas@12345]$ thread 45

输出示例:

"http-nio-8080-exec-3" Id=45 WAITING on java.util.concurrent.locks.ReentrantLock@3f5a2b1c owned by "http-nio-8080-exec-5" Id=47

    at java.lang.Object.wait(Native Method)
    - waiting on java.util.concurrent.locks.ReentrantLock@3f5a2b1c
    at com.example.service.InventoryService.deductStock(InventoryService.java:52)
    - locked java.util.concurrent.locks.ReentrantLock@7c8d9e0f <---- 但 blocks "http-nio-8080-exec-5"

可以看到线程 45 在等待锁 3f5a2b1c(被线程 47 持有),同时持有锁 7c8d9e0f(线程 47 在等待这把锁)。这就是经典的死锁环。

第三步:查看所有阻塞线程

# 查看所有状态为 BLOCKED 的线程
[arthas@12345]$ thread --state BLOCKED

第四步:确认锁竞争代码

# 反编译查看死锁涉及的代码
[arthas@12345]$ jad com.example.service.InventoryService deductStock

检查代码中的锁获取顺序。典型场景:方法 A 先锁 resource1 再锁 resource2,方法 B 先锁 resource2 再锁 resource1。

解决方案

  • 统一锁顺序:确保所有获取多把锁的代码路径使用相同的锁获取顺序。
  • 使用 tryLock 超时:用 tryLock(timeout) 替代 lock(),避免永久阻塞。
  • 减小锁粒度:用细粒度锁替代粗粒度锁,减少锁竞争。
  • 使用并发工具类:用 ConcurrentHashMapAtomicLong 等替代手动加锁。

常见死锁类型

死锁类型 特征 检测方式
互斥锁死锁 多线程互相等待 ReentrantLock/synchronized thread -b
线程池耗尽 所有线程被占满,新任务排队等待 thread 查看线程池线程状态
数据库锁等待 事务互相等待行锁 查看数据库 SHOW PROCESSLIST
循环等待外部资源 等待 HTTP 响应/消息回复 thread 堆栈可见 Socket.read

场景八:热更新代码紧急修复

问题现象

线上发现一个紧急 bug:某个判断条件写反了导致用户无法正常下单。重新发布需要走 CI/CD 流程,预计 30 分钟以上,但每分钟都在损失订单。需要立即修复。

排查思路

Arthas 支持 jad → mc → redefine 三步完成热更新:先反编译出源码,修改后重新编译,再加载替换运行中的类。整个过程不重启 JVM,秒级生效。

详细步骤

第一步:反编译目标类

# 反编译并只输出源码
[arthas@12345]$ jad --source-only com.example.service.OrderService > /tmp/OrderService.java

第二步:修改源码

在服务器上编辑 /tmp/OrderService.java,修复 bug(如将 if (status == 1) 改为 if (status != 1))。

# 使用 vim 或 sed 修改
vim /tmp/OrderService.java

第三步:内存编译

# 编译修改后的 Java 文件
[arthas@12345]$ mc /tmp/OrderService.java -d /tmp/output

输出示例:

Memory compiler output:
/tmp/output/com/example/service/OrderService.class

第四步:热加载替换

# 加载新的 class 文件替换运行中的类
[arthas@12345]$ redefine /tmp/output/com/example/service/OrderService.class

输出 redefine success 即表示热更新成功,新的代码逻辑立即生效。

第五步:验证修复

# 用 watch 观察方法行为是否已变更
[arthas@12345]$ watch com.example.service.OrderService createOrder "{params, returnObj}" -x 2

触发一次下单请求,确认返回结果正确。

注意事项与限制

限制项 说明
不能增删方法/字段 redefine 只能修改方法体,不能改变类结构(方法签名、字段数量)
不能修改继承关系 不能改变父类、接口列表
需要相同 ClassLoader 多 ClassLoader 场景需指定 -c <classLoaderHash>
不持久化 JVM 重启后恢复原代码,仍需通过正式发布修复
需要 JDK mc 命令依赖 JDK 的编译器,JRE 环境不可用

指定 ClassLoader 的热更新(Spring Boot 场景):

# 先查看类的 ClassLoaderHash
[arthas@12345]$ sc -d com.example.service.OrderService | grep classLoaderHash
# classLoaderHash 5a54a66

# 编译时指定 ClassLoader
[arthas@12345]$ mc -c 5a54a66 /tmp/OrderService.java -d /tmp/output

# redefine 时也指定
[arthas@12345]$ redefine -c 5a54a66 /tmp/output/com/example/service/OrderService.class

场景九:数据库连接池耗尽排查

问题现象

应用间歇性报 Cannot get a connection, pool error: Timeout waiting for idle object。大量请求堆积超时,但数据库本身负载正常。

排查思路

连接池耗尽通常有三种原因:一是连接泄漏(借出后未归还),二是慢 SQL 占用连接时间过长,三是连接池配置过小。需要查看连接池状态和连接使用情况。

详细步骤

第一步:查看连接池实例状态

# 查找连接池对象实例(以 HikariCP 为例)
[arthas@12345]$ vmtool --action getInstances --className com.zaxxer.hikari.HikariDataSource --limit 5

第二步:查看连接池内部状态

# 通过 OGNL 查看连接池的关键指标
[arthas@12345]$ ognl '#data = @com.zaxxer.hikari.HikariDataSource@class, \
  #instances = #data.getName(), \
  #pool = #data.getDeclaredField("pool"), \
  #pool.setAccessible(true), \
  #hikariPool = #pool.get(null), \
  #hikariPool.toString()' -c 5a54a66

更实用的方式是直接查看连接池的统计信息:

# 查看连接池配置和当前状态
[arthas@12345]$ getstatic com.zaxxer.hikari.HikariPoolMXBean poolName
[arthas@12345]$ vmtool --action getInstances --className com.zaxxer.hikari.HikariPool --limit 1 \
  --express 'instances[0]'

第三步:追踪连接获取耗时

# 追踪 getConnection 方法的耗时
[arthas@12345]$ trace com.zaxxer.hikari.HikariDataSource getConnection -n 5

如果 getConnection 耗时很长,说明连接池中确实没有可用连接。

第四步:排查慢 SQL 占用连接

# 观察数据库查询方法的执行时间
[arthas@12345]$ trace org.springframework.jdbc.core.JdbcTemplate query '#cost > 1000' -n 10

如果发现有 SQL 执行超过 1 秒,说明是慢 SQL 占用了连接。

第五步:排查连接泄漏

# 监控连接的借出和归还
[arthas@12345]$ watch com.zaxxer.hikari.pool.PoolEntry getConnection "{params, returnObj}" -x 1 -n 20

观察连接是否被正常归还。如果借出数量持续增长但归还数量不增长,说明存在泄漏。

第六步:查看线程持有哪些连接

# 查看哪些线程在等待连接
[arthas@12345]$ thread --state WAITING | grep -i "getConnection\|Hikari"

解决方案

根因 解决方案
连接泄漏 检查所有 getConnection 调用路径,确保在 finally 中归还;使用 try-with-resources
慢 SQL 优化 SQL、加索引;设置 SQL 超时时间
连接池过小 根据实际并发量调大 maximumPoolSize
未设置超时 配置 connectionTimeout,避免无限等待
事务未提交/回滚 检查 @Transactional 注解的方法是否有异常未回滚的情况

场景十:特定用户数据问题排查

问题现象

某个特定用户(userId=12345)反馈下单异常,但其他用户正常。本地无法复现,因为本地没有该用户的数据环境。需要在生产环境中针对该用户的数据进行诊断。

排查思路

这是 Arthas 最典型的应用场景——在生产环境针对特定条件的方法调用进行观测。核心是利用 watch 的条件表达式,只在特定用户调用时输出观测数据。

详细步骤

第一步:条件观测方法入参

# 只在 userId=12345 的调用时输出入参和返回值
[arthas@12345]$ watch com.example.service.OrderService createOrder \
  "{params, returnObj}" \
  "params[0].userId == 12345" \
  -x 3

让用户再操作一次,Arthas 会捕获该次调用的完整入参和返回值。

第二步:追踪该用户的调用链

# 条件追踪,只在特定用户调用时追踪耗时
[arthas@12345]$ trace com.example.service.OrderService createOrder \
  'params[0].userId == 12345' \
  -n 1

观察该用户的调用链中哪个环节出了问题。可能发现该用户的某个关联数据异常(如收货地址为空、优惠券已过期等)导致后续逻辑出错。

第三步:用 OGNL 直接查询该用户的数据

# 直接调用 Service 方法查询该用户数据(不影响业务,只读操作)
[arthas@12345]$ ognl '@com.example.SpringContextHolder@getBean(@com.example.service.UserDataService@class).getUserData(12345)' -x 3

或者在有 Spring 上下文的情况下:

# 通过 Spring ApplicationContext 获取 Bean 并调用方法
[arthas@12345]$ ognl '#context = @org.springframework.web.context.ContextLoader@getCurrentWebApplicationContext(), \
  #service = #context.getBean("userService"), \
  #service.getUserProfile(12345)' -x 3

第四步:用 tt 记录并回放调用

如果问题偶发且用户难以配合复现,可以先用 tt 持续记录方法调用,等问题出现后回放:

# 记录方法所有调用
[arthas@12345]$ tt -t com.example.service.OrderService createOrder

等一段时间后查看记录:

# 查看所有记录
[arthas@12345]$ tt -l

找到异常的那次调用(通过时间或返回值判断):

# 查看某次调用的详细信息
[arthas@12345]$ tt -i 1000

# 重放某次调用(重新执行该方法,使用记录时的入参)
[arthas@12345]$ tt -i 1000 -p

-p (play) 会用记录时的入参重新执行一次方法调用,可以在修复后验证逻辑是否正确。

tt 命令核心用法

tt -t <class> <method>     开始记录方法调用
tt -l                       列出所有记录
tt -i <index>               查看指定记录详情
tt -i <index> -p            重放指定记录
tt --delete <index>         删除指定记录
tt --delete-all             清除所有记录

五、高级技巧

5.1 OGNL 表达式实战

OGNL(Object-Graph Navigation Language)是 Arthas 的表达式引擎,watchtracett 等命令的条件表达式和观测表达式都基于 OGNL。掌握 OGNL 可以实现非常灵活的诊断逻辑。

常用 OGNL 技巧:

# 1. 调用静态方法
ognl '@java.lang.System@getProperty("java.version")'

# 2. 获取 Spring Bean 并调用方法
ognl '#context = @org.springframework.web.context.ContextLoader@getCurrentWebApplicationContext(), #bean = #context.getBean("myService"), #bean.doSomething()'

# 3. 在 watch 中构造复杂条件
watch com.example.service.OrderService createOrder "{params, returnObj}" \
  "params[0].userId == 12345 && #cost > 500 && returnObj != null" -x 3

# 4. 在 watch 中调用方法处理参数
watch com.example.service.OrderService createOrder \
  "@com.example.JsonUtils@toJson(params[0])" -x 1

# 5. 获取方法执行时间并做条件判断
watch com.example.service.OrderService createOrder \
  "{params, #cost}" "#cost > 1000" -x 2

# 6. 获取目标对象的字段值
watch com.example.service.OrderService createOrder \
  "target.cacheMap.size" -x 1

5.2 后台异步任务

当问题不确定何时出现,需要长时间监控某个方法时,可以将命令放到后台运行:

# 在后台执行 watch 命令,加 & 表示后台运行
[arthas@12345]$ watch com.example.service.OrderService createOrder "{params, returnObj}" -x 2 &

# 查看后台任务
[arthas@12345]$ jobs

# 查看后台任务输出
[arthas@12345]$ jobs -d

# 停止后台任务
[arthas@12345]$ jobs -k <job_id>

配合日志保存功能,可以把后台任务的输出写入文件,事后分析:

# 启动时重定向输出到文件
[arthas@12345]$ watch com.example.service.OrderService createOrder "{params}" -x 2 >> /tmp/arthas-watch.log &

5.3 profiler 生成火焰图

当需要做深度性能分析时,火焰图是最直观的工具:

# 开始采集(默认采样 CPU)
[arthas@12345]$ profiler start

# 等待一段时间(30秒~几分钟)

# 停止采集并生成火焰图
[arthas@12345]$ profiler stop --format html

生成的 HTML 火焰图可以直接在浏览器中打开查看。火焰图的横轴是采样数量(代表 CPU 占用),纵轴是调用栈深度。宽度越宽的方法,占用的 CPU 时间越多。

# 采集内存分配火焰图
[arthas@12345]$ profiler start --event alloc

# 采集指定时间后自动停止
[arthas@12345]$ profiler start --duration 60

5.4 批处理命令

需要一次性执行多条命令时,可以将命令写入文件,用 batch 模式执行:

# 创建命令文件
cat > /tmp/arthas-commands.txt << 'EOF'
dashboard -n 1
thread -n 5
sc -d com.example.service.OrderService
jad com.example.service.OrderService
EOF

# 批量执行
java -jar arthas-boot.jar 12345 -f /tmp/arthas-commands.txt

5.5 条件断点式排查

结合 watch 的条件表达式,可以实现类似 IDE 条件断点的效果:

# 模拟"条件断点":当订单金额大于 10000 且用户为 VIP 时才观测
watch com.example.service.OrderService createOrder \
  "{params, returnObj, target}" \
  "params[0].amount > 10000 && params[0].userLevel == 'VIP'" \
  -x 4 -n 1

-n 1 表示只匹配一次就停止,相当于"命中一次断点"。

5.6 多 ClassLoader 场景处理

Spring Boot、OSGi、Tomcat 等环境下存在多个 ClassLoader,需要指定正确的 ClassLoader:

# 查找类时指定 ClassLoaderHash
[arthas@12345]$ sc -d com.example.service.OrderService
# 输出中找到 classLoaderHash: 5a54a66

# 所有命令都可以通过 -c 指定 ClassLoader
[arthas@12345]$ watch -c 5a54a66 com.example.service.OrderService createOrder "{params}" -x 2
[arthas@12345]$ jad -c 5a54a66 com.example.service.OrderService
[arthas@12345]$ redefine -c 5a54a66 /tmp/OrderService.class

六、生产环境注意事项

6.1 性能影响控制

Arthas 使用字节码增强技术,对目标方法织入观测逻辑,会带来一定的性能开销。生产环境使用时需要注意:

措施 说明
始终设置 -n 参数 限制匹配次数,避免无限期增强方法。-n 5 表示匹配 5 次后自动停止
避免对高频方法长时间监控 QPS 上万的方法,watch/trace 持续运行会显著影响性能
排查完毕及时停止 stop 或 Ctrl+C 停止所有增强,恢复原始字节码
使用条件过滤 通过条件表达式减少观测输出量,降低序列化和 I/O 开销
避开流量高峰 尽量在低峰期执行重操作(如 heapdumpprofiler

6.2 安全注意事项

风险点 防护建议
Arthas 可执行任意代码 生产环境严格限制 Arthas 访问权限,不暴露 Web Console 到公网
连接端口安全 默认 telnet 3658 / HTTP 8563 端口不鉴权,需通过网络策略隔离
敏感数据泄露 watch 可能输出包含用户隐私的参数,注意脱敏
热更新风险 redefine 可能引入新 bug,建议先在测试环境验证
操作审计 启用 --stat-url 记录所有执行命令,便于审计追溯

6.3 最佳实践清单

  1. dashboard 后深入——先看全局指标,再针对性深入,避免盲目猜测。
  2. trace 优于 watch 做性能分析——trace 展示调用链耗时分布,watch 侧重数据观测。
  3. 善用条件表达式——减少噪声,精准定位。
  4. -n 必加——任何观测命令都应设置匹配次数上限。
  5. 用完即 stop——排查完毕后执行 stop 清理增强,避免长期影响。
  6. 热更新要谨慎——redefine 仅作紧急止血,正式修复仍需走发布流程。
  7. 保留操作记录——重要排查过程保存日志,便于复盘和知识沉淀。
  8. 建立团队命令库——将常用排查命令整理成脚本,新成员可快速上手。

6.4 常见问题与排错

问题 原因 解决方案
attach 失败 JDK 版本不匹配或权限不足 确认 Arthas 版本支持目标 JDK;用与应用相同的 JDK 启动 Arthas
命令无输出 类未加载或 ClassLoader 不对 sc 确认类已加载;用 -c 指定正确的 ClassLoaderHash
mc 编译失败 JDK 版本或缺少依赖 确保使用 JDK(非 JRE);检查源码中的 import 是否可用
redefine 报错 类结构变更或签名不匹配 确保只修改方法体,不增删方法/字段;使用相同 ClassLoader 编译
trace 输出不全 默认跳过 JDK 方法 --skipJDKMethod false 查看完整链路
watch 看不到内部字段 展开层级不够 增加 -x 参数值(如 -x 4

七、常用命令速查表

7.1 一分钟定位命令

# 看全局
dashboard

# 找最忙线程
thread -n 3

# 看线程堆栈
thread <thread_id>

# 反编译确认代码
jad <class> <method>

# 追踪方法耗时
trace <class> <method> -n 5

# 观测入参出参
watch <class> <method> "{params, returnObj}" -x 3

# 查看日志级别
logger

# 改日志级别
logger --name <package> --level DEBUG

# 查看内存
memory

# 查堆内对象
vmtool --action getInstances --className <class> --limit 10

# 查看静态字段
getstatic <class> <field>

# 查看已加载类
sc -d <class>

# 执行 OGNL
ognl '<expression>'

# 热更新
jad --source-only <class> > /tmp/X.java  # 反编译
mc /tmp/X.java -d /tmp/output            # 编译
redefine /tmp/output/<path>/X.class      # 加载

# 火焰图
profiler start
profiler stop --format html

# 退出
quit        # 退出会话
stop        # 彻底卸载

7.2 命令参数速查

watch 内置变量:

变量 含义
params 入参数组,params[0] 为第一个参数
returnObj 返回值
throwExp 抛出的异常对象
target 方法所在对象实例
#cost 方法执行耗时(毫秒)

常用条件表达式示例:

#cost > 500                          # 耗时超过 500ms
params[0].userId == 12345            # 特定用户
#throwExp != null                    # 抛出异常时
returnObj == null                    # 返回值为 null
params[0].userId == 12345 && #cost > 500   # 组合条件

trace/ watch 公共参数:

参数 含义
-n <N> 匹配次数上限(强烈建议每次都加)
-x <N> 对象展开层级(watch 用)
-c <hash> 指定 ClassLoader
-v 打印条件表达式执行结果
#cost 耗时(ms),用于条件过滤

7.3 场景到命令映射表

排查场景 首选命令 辅助命令
CPU 飙高 thread -n trace, jad, profiler
接口慢 trace watch, monitor
方法异常 watch stack, jad
内存泄漏/OOM memory, vmtool dashboard, heapdump
日志不够 logger ognl
类冲突 sc -d, jad dump, classloader
死锁 thread -b thread --state BLOCKED
热修复 jadmcredefine ognl
连接池耗尽 vmtool, trace watch, thread
特定用户问题 watch (条件) tt, ognl
性能深度分析 profiler trace, monitor
查看方法调用统计 monitor tt

7.4 危险操作警告

以下操作在生产环境需格外谨慎,建议在低峰期执行并做好回滚准备:

操作 风险等级 风险说明
redefine 热更新代码可能引入新 bug,且不持久化(重启后丢失)
heapdump 导出堆 dump 会暂停应用(STW),大堆可能暂停数秒
profiler start 采样有性能开销,长时间运行会影响吞吐量
ognl 调用写方法 直接调用 Service 写方法会修改业务数据
thread 暂停线程 暂停业务线程可能导致请求超时
-nwatch/trace 高频方法持续增强会显著影响性能

结语:Arthas 的核心价值在于"不重启、不改代码、实时观测"。熟练掌握 dashboard → thread → trace → watch → jad 这条核心排查链路,配合条件表达式和 OGNL,可以覆盖 90% 以上的线上问题排查需求。建议在日常开发中多加练习,形成肌肉记忆,线上出问题时才能快速定位、从容应对。