“线上又报警了,接口超时、CPU飙高、内存吃紧,赶紧先重启一下压压惊。”这话我太熟了——Java应用跑在生产环境,平常还好,一遇到诡异问题就束手束脚。不敢随便开debug端口,不能随意改日志级别,更别提加日志重新发版,审批流程走过去,事故早凉透了。Arthas就是用来改变这个局面的工具:它能在不重启JVM的前提下,对一个正在运行的Java进程做实时诊断,方法入参、出参、异常、调用链、线程状态、类加载信息,统统可以当场抓出来。这篇不是官方文档复读,是把我自己在故障现场用Arthas处理问题的完整思路和踩坑记录整理出来,适合正在被线上问题折磨的Java后端开发、运维以及SRE同学参考。
1. 排查线上问题时,我经历过的三板斧困境
1.1 重启大法为什么常常不灵
先说个最典型的场景。某个订单服务从下午开始偶发超时,客户那边反馈“点提交一直转圈”,监控上看TP99从200ms涨到2800ms,但集群里十几台机器只有一两台有异常。这种时候如果直接重启“嫌疑”实例,确实可能暂时恢复,但问题根因还留在代码里,过几个小时它还会回来。更麻烦的是,重启意味着丢失现场——线程堆栈没了、请求参数没了、JVM的即时状态也没了,想复盘只能靠猜。
另一个我踩过的坑是“加日志看情况”。很多人会临时在关键方法里塞几行log.info,然后走发布流程重新上线。可生产环境日志量本身就大,加上日志级别调整要连同代码一起发布,等到日志打到文件里、再捞出来分析,十分钟过去都是快的。有一次为了定位一个只在凌晨三点出现的异常,我们前后发版四次,折腾了一整周。重启和重新发布之所以效率低,本质原因是它们都在“远离真相”:把现场销毁了,把状态重置了,然后靠概率去复现问题。
1.2 Arthas在故障排查中到底扮演什么角色
Arthas的定位是“在线诊断”,核心价值在于:在不停止应用、不重新发布的情况下,直接connect到目标JVM进程里做实时命令式检查。它由Alibaba开源,基于Java Agent机制实现,2018年开源后迅速成为Java后端排查工具的标配之一。我在实际使用中最看重它的三件事:第一,动态查看方法调用现场,包括入参、出参、抛出的异常,这对于定位“某个偶发请求为什么失败”几乎是决定性的;第二,不需要在业务代码里预先埋点,Arthas通过字节码增强方式运行,对业务代码零侵入;第三,它支持在运行中修改类的字节码实现,也就是代码热更新,虽然这事要谨慎,但在紧急止损时能救命。
很多人会把Arthas和BTrace放在一起比较,或者觉得它有类似功能和JDK自带的jstack、jmap、jstat是不是重复了。其实Arthas更像是把这些JDK命令行工具的能力“交互式”整合到了一起,并且补齐了查看方法调用参数、反编译类、热更新这类更高级的能力。它对标的不是某一个工具,而是“你无法在线上现场停下来慢慢分析”这个根本痛点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Arthas核心原理与安装:为什么能不停机做检查
2.1 Java Agent与字节码增强的底层逻辑
要理解Arthas为什么能做到“动态诊断”,需要先理解Java Agent机制。Java Agent可以理解为JVM中的一种特殊插件,它能够在类被加载时或运行过程中,对字节码进行转换。Arthas在启动时会attach到目标JVM上,以Agent的身份加载自己的诊断模块;当你输入watch、trace这样的命令时,Arthas会对目标类做字节码增强,在原方法上织入逻辑,拦截方法执行的调用事件、返回事件和异常事件,然后把你想要的信息输出到交互终端。
用大白话说,这相当于在运行中的代码里装了一批“临时监控摄像头”,摄像头装好了你随时看现场,看完拆掉,业务进程本身毫发无损。这也是Arthas“无需重启”的底气来源。需要注意,字节码增强不是改写磁盘上的class文件,只作用于当前JVM运行时的内存态,所以重启应用后增强效果会全部消失。这既是优点(安全、可回滚),也是坑(有些新手以为改了类就永久生效,结果一重启问题又来了)。
2.2 下载安装与启动的几种常用姿势
Arthas支持多种安装方式。最简单的一种是直接用arthas-boot:
bash复制curl -L -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
启动后它会列出当前机器上正在运行的Java进程,输入序号回车就能attach到目标进程。如果你希望脚本化操作,也可以用as.sh一键安装脚本,或者把arthas-boot.jar放到固定目录里作为团队运维工具统一管理。我个人的习惯是在本地和跳板机上各放一份,服务器上不装任何额外东西,因为arthas的Agent运行本身不依赖服务器上的安装目录,只要能从运维机发起attach即可。
进入Arthas交互界面后,默认通过Telnet连接,端口是3658,如果端口被占用可以加参数换个端口:
bash复制java -jar arthas-boot.jar --telnet-port 9999 --http-port 8899
还有一个容易被忽略的启动问题:如果你运行java -jar arthas-boot.jar之后列表里看不到目标进程,可以先确认运行Arthas的账号和Java应用是否同一个系统用户,以及是否处于容器环境中。容器场景下经常出现PID命名空间隔离的问题,如果Arthas跑在宿主机、Java应用跑在容器里,attach大概率会失败。
2.3 Arthas和JDK原生命令工具的效率对比
有人可能会说,jstack、jmap能拿线程堆栈、能导堆转储,为什么还要用Arthas?这里可以把它们放在一起看:
| 能力维度 | JDK原生工具 | Arthas |
|---|---|---|
| 查看线程堆栈 | jstack,只能看瞬时快照 | thread,支持看CPU占用Top N、线程状态过滤、阻塞线程定位 |
| 导出堆内存 | jmap,导出整个堆,文件大分析慢 | heapdump,同样能导,但可配合vmtool快速筛对象 |
| 反编译代码 | 无,javap只能看字节码无法看源码 | jad,直接还原成可读性较高的Java源码 |
| 查看方法入参出参 | 无法实现 | watch,随时对目标方法设监听 |
| 调用链耗时拆分 | 无 | trace,精确到每一层子调用耗时 |
| 热更新 | 无 | jad + mc + redefine |
| 修改日志级别 | 需发版 | logger命令动态调整 |
从这个表格能看出来,jstack这类工具解决的是“事后快照”问题,而Arthas解决的是“现场持续观测”问题。遇到真正难以复现的偶发故障,后者才有机会抓到第一手线索。
3. 入口级命令:先给进程做个体检
3.1 dashboard命令看整体运行状态
attach成功后的第一件事,我通常是输入dashboard,它会以仪表盘形式实时展示当前JVM的整体运行状态,包括线程总览、GC次数、堆内存使用情况等。
Arthas的dashboard输出大致分成几个区块:顶部是线程概览,会显示线程ID、名称、CPU占用率等;中间是JVM内存信息,包括堆内存各区间的used、committed、max;下方还有GC相关的统计。看这个界面的目的是快速建立判断:
- 如果CPU集中在少数几个业务线程上,说明可能存在死循环或者大计算量;
- 如果Old区持续上涨且GC频繁,大概率存在内存泄漏或大对象分配;
- 如果线程数异常多,很多处于WAITING状态,可能是线程池被打满。
有个细节:dashboard是持续刷新的,按q退出,或者Ctrl+C也可以中断回命令行。如果只想看一次立即退出,可以用dashboard -i 5000 -n 1这类参数控制刷新间隔和次数,方便脚本采集。
3.2 thread命令定位线程级别的CPU高与阻塞
线上CPU飙高是后端最常见的“疑难杂症”之一。通用做法是用top -Hp找到CPU高的线程号,再转16进制去jstack里搜小线程号,整个过程繁琐且容易看错。Arthas把这事直接封装了:输入thread -n 3就能看到当前CPU占用最高的三个线程及其堆栈。
如果是死锁问题,用thread -b可以直接找出阻塞其他线程的阻塞线程。还有一个非常实用的用法是按状态过滤:
bash复制thread --state WAITING
这能看到所有处于WAITING状态的线程,结合线程名判断是不是线程池里堆积了过多空闲任务。thread命令本质上是对ThreadMXBean的封装,但它胜在输出友好,节省了大量手工转换过程。
3.3 类与类加载器的基础侦查
Java应用跑在容器里,类加载器问题让很多人头疼。一个常见的故障是依赖冲突,某个类明明在本地能跑、测试环境能跑,生产环境却报ClassNotFoundException或NoSuchMethodError。用Arthas的sc(search class)命令可以快速确认目标类是否被加载、是被哪个类加载器加载的:
bash复制sc -d com.example.service.OrderServiceImpl
输出里能看到具体的ClassLoader及其hash值。再配合classloader命令查看整个应用里有哪些类加载器,以及每个加载器加载的类数量,基本能快速圈定是不是某个依赖包被多个不同版本同时加载了。运行时你是没法直接打开IDE看classpath的,这类场景下sc就是一把尺子。
4. 调用链的“显微镜”:watch、trace、stack如何定位问题
4.1 watch命令的完整参数拆解
watch是Arthas里使用频率最高、价值最直接的命令,它的作用是观察一个方法被调用时的信息,包括入参、返回对象、抛出的异常。基本格式是:
bash复制watch com.example.service.OrderService createOrder '{params, returnObj, throwExp}' -x 3 -n 5
com.example.service.OrderService命名空间下的类名,可以用通配符匹配;createOrder是方法名;'{params, returnObj, throwExp}'是输出表达式,从OGNL表达式中取值,分别代表入参、返回结果、异常;-x指定输出结果遍历深度,层级深的对象默认只打印一层,要看到嵌套字段需要增加深度;-n限制观察次数,防止生产环境无限输出导致大量日志。
默认情况下watch只观察方法正常返回(-f可以在返回后增强,-b在调用前观察)。我踩过一个尴尬事:线上用watch一个小方法,半天没有输出,后来发现该接口走的是异步调用,真正的执行不在当前线程里,所以始终命中不了。这种情况下可以自行判断是否需要观察入口方法的前置节点。
4.2 trace命令把方法耗时拆到“每一层”
如果你的接口整体很慢,但不知道慢在哪一行代码,不要用猜的,直接用trace。
bash复制trace com.example.service.OrderService createOrder '#cost > 100'
这个命令执行后会打印方法的调用链,按每一个子调用展示各自耗时。后面的#cost > 100是条件表达式,意思是只打印总耗时超过100ms的调用。收到输出后你能一眼看穿瓶颈是在数据库访问、RPC调用还是本地计算上。
trace原理上也是字节码增强,它会在方法进入和退出时记录纳秒级时间戳,然后汇总出子调用耗时。这是我对Arthas评价最高的能力之一,因为它把原来需要依赖APM系统(链路追踪平台)才能看到的信息,变成了一个命令就能解决的轻量手段。尤其是那些没有接入全链路追踪的老项目,trace几乎是唯一的挖耗时方案。
4.3 stack命令回答“谁在调用我”这个经典问题
排查问题时还有一个高频疑问:这个方法被请求调到了不奇怪,但为什么会有奇奇怪怪的上游把请求发过来?stack命令用来查看某个方法的调用栈路径:
bash复制stack com.example.service.OrderService createOrder
输出会列出当前方法调用时刻的完整线程调用栈,并且持续监听。这对于理解一次异常的请求来源极有帮助。我会把stack当成“反向追踪”工具,正常时候不用,但每次排查诡异跨服务调用时都靠它指明方向。
4.4 监听类命令的常规注意事项
使用watch、trace、stack这些监听命令,要清楚它们本质上是在JVM里临时注入拦截逻辑,虽然Arthas会保证在退出时恢复字节码,但如果你开启一个无限监听的命令且一直不退出,会对性能有影响。我自己的习惯是:
- 生产环境默认都要加
-n 1或-n 3,看几条结果就自动停止; - 用条件表达式缩小监听范围,比如只关注订单金额超过某个阈值的请求,避免所有请求都被拦截增强;
- 线上高峰期不要做大范围的trace,尽量挑一台低流量实例做实验。
5. 免发版热更新:jad/mc/redefine与logger动态调级的完整流程
5.1 热更之前先搞清楚边界
Arthas可以在运行中重新定义类方法实现,这就是jad、mc、redefine三个命令的组合拳。网上对这块的期待非常高,好像线上发现问题可以一改了之,但实际要冷静:redefine不能新增或删除字段、方法,也不能修改方法签名,只能替换已有方法的方法体实现。换句话说,它能修的是方法里的具体逻辑,比如某个判断条件写错了、某个返回值算错了,但没法新增一个新的private方法然后redefine。
基于这个边界,我的态度是把redefine当作“紧急止血”手段,而不是常规发版替代方案。它适用于一个明确且改动很小的逻辑错误,如果你要对代码做大调整,或者新增依赖调用,那还是老老实实走发布流程。
5.2 从反编译到重新编译的完整操作步骤
流程分三步。
第一步,用jad反编译目标类:
bash复制jad --source-only com.example.service.OrderServiceImpl > /tmp/OrderServiceImpl.java
--source-only是只输出源码部分,比默认输出更干净,方便直接修改。拿到源码后,把要改的地方改好,比如修复那个溢出风险的条件判断。
第二步,用mc在内存里编译修改后的类:
bash复制mc -c <classLoaderHash> /tmp/OrderServiceImpl.java -d /tmp/output
-c参数指定目标类的ClassLoader hash值,用sc -d能看到。如果编译报错大概率是缺依赖JAR,可以用--source-root指定项目根目录,让mc能从classpath里面找到更多依赖。
第三步,用redefine把编译后的class文件重新加载:
bash复制redefine /tmp/output/com/example/service/OrderServiceImpl.class
执行成功后,Arthas会提示redefine success,并列出影响到的类。这就完成了热更。注意热更只在当前进程生效,代码里若需要保留修复,要立刻把改动同步到源码并正常走一次发版。
5.3 日志级别的“无痛动态调整”
和热更相比,动态调整日志级别是风险更低、用得更多的能力。很多线上问题难排查,第一原因其实是日志不够——业务日志全是info级别,debug级别的细节根本看不到。
Arthas的logger命令可以直接修改指定Logger的日志级别:
bash复制logger --name ROOT --level debug
执行完以后再看日志,原来被过滤掉的debug信息就输出了。问题定位完再把它调回info。这个命令对log4j2、logback都适用(推荐使用新版,支持面更广)。曾经有次我们排查一个诡异的空指针,堆栈指向的类被本地代码混淆过,根本看不出业务调用点,就是靠临时把Dubbo消费方的provider调用链路的logger级别调到debug,定位到了参数来源。
从运维规范上看,logger命令这种临时改级别的方式相比发布时改配置要快得多,也就是所谓的“诊断手段动态化”。只要记得在结束后恢复,它本身非常安全。
6. tt命令:把线上请求“录像回放”,而不是盲目复现
6.1 tt能做什么,和watch有什么本质区别
tt(TimeTunnel)是Arthas里比较有想象力的一个功能,它能把某个方法的调用记录保存下来,包括入参、返回结果、当前对象状态等,之后你可以在任意时刻查看甚至重放这次调用。
和watch区别在哪儿?watch像看实时摄像头,盯着现场;tt像个DVR录像机,先录下来,事后反复看、甚至可以重新跑一遍当时的动作。
典型用法是:
bash复制tt -t com.example.service.OrderService createOrder
执行后去触发一个请求,命令窗口会打印一条调用记录,包括index编号和耗时。录完之后用tt -i 1000根据索引查看详细信息:
bash复制tt -i 1000
想看到入参对象的完整字段,可以配合-w表达式(watch表达式写法基于ognl):
bash复制tt -i 1000 -w 'params[0]'
遇到偶发性问题,我一般开着tt录制目标方法,等异常请求出现一次,就把记录的详细参数存下来,再通过回归测试构造同样的条件。tt对定位那种“用户说有问题但我复现不了”的疑难问题尤其好用。
6.2 重放功能的边界与内存占用警惕
tt还能重放之前的调用记录:tt -i 1000 -p会重新调用一次当时记录的方法。不过重放用的只是方法的入参,对于那些依赖外部系统状态变化的场景,比如依赖数据库里的数据已经变了,重放结果不一定能还原当初的失败现场,这块要心里有数。
更值得警惕的是内存占用。tt会在JVM内部缓存调用记录,缓存过大时会造成额外的内存开销甚至诱发OOM。因此我使用tt有一个铁律:问题抓完立刻执行tt --delete-all清空所有记录,不让无用的recording堆积在生产进程里。
7. ognl表达式:当常规命令不够用时,用代码级操作兜底
7.1 为什么需要ognl而不满足于watch
watch、trace这类命令好用,但它们的输出格式相对固定,表达复杂逻辑时力不从心。有些排查需要直接读特定对象里一个Map的某个key对应的value,或者需要直接执行某个static方法拿结果,用watch写起来特别别扭。这时候用Arthas内置的ognl命令可以做更强的表达式求值。
比如在发现某个服务实例里有一个状态变量出了问题,你想直接看这个实例现在某个静态字段的值:
bash复制ognl '@com.example.config.DynamicConfig@isGrayRelease()'
也可以结合vmtool找到某个类的具体实例,再对这个实例查询内部字段。比方说,怀疑某个线程池的核心线程被全部打满:
bash复制vmtool --action getInstances --className java.util.concurrent.ThreadPoolExecutor --limit 10 -x 2
再用ognl去查看对应实例的队列长度和活跃线程数量。这已经非常接近“钻进JVM里随便摸数据”的效果了。
7.2 最容易踩的类加载器上下文问题
用ognl执行“看某个静态字段”这类操作时,一个很常见的报错是No such field或者类找不到,但你的类名明明是对的。原因多半是目标类不在ognl默认的ClassLoader搜索路径下,尤其Spring Boot项目里很多类是LaunchedURLClassLoader加载的。
解决办法是显式指定ClassLoader(3.5.7之后也支持按类名指定):
bash复制ognl --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader '@com.example.service.ConfigHolder@configMap'
给ognl加上类加载器上下文之后,像是解锁了权限,能访问的范围大大增加。这个坑几乎是我每次带新人用ognl必讲的一课——很多人卡在这一步,误以为工具不好使,其实只差了一个--classLoaderClass参数。
8. 我把底层能力串起来用的实战:一次偶发超时的完整排查链路
8.1 从头到尾的排查步骤演示
以前段时间处理的一个生产问题为例:订单提交接口每天偶发超时,一天大概出现两三次,每次持续一分钟左右自动恢复,日志里没有异常堆栈。刚开始客户不认可重启解决,希望彻底查清根因。那次的处理步骤可以说把上面提到的命令都用上了,整理成一条完整排查链路供参考:
第一步,用top找到可疑实例的进程PID,Arthas attach进去。
第二步,输入dashboard观察整体。内存和GC没有明显异常,旧年代增长匀速,说明问题不太可能是内存泄漏。但右下角持续刷新的线程列表里,发现几个业务线程状态大量时间是WAITING,这个细节激起了注意。
第三步,执行thread -n 5查CPU占用最高的线程,发现异常线程CPU不高,进一步用thread查看对应线程的完整堆栈,看到大量线程阻塞在CountDownLatch.await上,且等待时间超过15秒——这基本确定了问题方向:某接口等一个异步任务迟迟不返回。
第四步,用watch观察订单服务主处理方法的返回情况,发现返回前总是先执行一个异步回调,回调完成时间不可控。用trace把回调执行链路拆开看,发现大部分时间耗在一个内部消息推送组件中。
第五步,用stack查看是谁在调用这个耗时组件,确认是一个全局的重试机制导致所有节点同时尝试重试,形成短暂的消息堆积。
第六步,用jad反编译组件的重试触发方法,确认判断逻辑中有一个等待时间计算错了,把exponential backoff参数用反了。这个修复点很小,我们先用mc+redefine在一块低流量实例上做了热更验证,超时现象立刻不再出现。随后再按流程发版修复。
8.2 排查过程中的关键判断与经验沉淀
整个过程中Arthas提供的最大价值不是某一个命令,而是让我们在问题发生时能“就地取材”。传统做法里,这种问题要么靠猜、靠调参数一个个试,要么只能加日志等下一次发作,但Arthas给了直接在现场抽丝剥茧的能力。
要强调一下使用纪律:任何redefine改动都会影响线上实例,所以必须在执行前保留原jad源码、记录改动点,最好让另一位同事做review。那次实战我们在热更前把修改文件复制到本地Git仓库留档,发版时还同步改了原代码,防止重启后Arthas的修复被冲掉却没人发现。
9. Arthas使用的边界、安全风险与我的日常习惯
9.1 哪些场景不适合用Arthas
Arthas不是万能的,这一点要清醒认识。
- 如果问题发生在进程外部,比如网络不通、下游服务无响应,Arthas的诊断能力只能帮你看调用方角度,不能代替底层网络排查;
- 如果进程处于严重OOM边缘,attach一个新的Agent给JVM本身就会增加内存占用,严重的可能加速进程宕掉。遇到这种情况不如先立即重启止血或者dump保留现场;
- 如果目标应用用了非常规的安全策略或定制的ClassLoader管理,Arthas的增强可能会失败或报错。
另一个不该用的场景是:你已经明确问题需要改架构才能解决,就没必要花时间在Arthas里反复观看细节,工具帮你定位到根因而止,后续该refactor就refactor,不要依赖热更长期扛着落后的代码运行。
9.2 我在生产环境使用Arthas的几个工作习惯
最后分享几条自己长期养成的实操规矩,供有类似需求的同学参考。
- attach和操作全走堡垒机/运维账号,Arthas的Agent具备很强能力,尤其是ognl、redefine这类命令可以改运行中进程,一定要纳入权限管理,避免随意使用。
- 所有人使用前统一培训基础命令和安全边界,至少让他们知道watch别把输出打到公共日志里、ognl不要在生产环境执行未知表达式、redefine必须Review。Arthas只是一个工具,出问题的是误用它的人。
- 对生产进程执行操作时,建议开通第二条登录会话,一个会话跑监听命令,另一个会话随时准备处理突发情况,这样一旦命令卡住,不至于把自己锁死在交互界面旁边。
- 尽量在测试环境预演一遍要执行的命令,因为Arthas命令里OGNL表达式的转义在Shell里很容易失手。本地先把反斜杠、引号、特殊字符调好,线上执行能一次命中,少很多不必要的时间损耗。
- 保持Arthas版本更新。早期版本对JDK 17+的兼容性并不好,新版本会持续增强对模块化JDK的支持,所以Java版本升到高版本之后,Arthas也要一起升级,版本不匹配会碰到attach失败、增强不了Module类等问题。
我见过不少团队刚引入Arthas时很兴奋,拿着它各种命令一通测试,但真正遇到问题时又忘得干干净净,回去了老一套加日志重启的老路。Arthas不是像IDE调试器那样“断点一打、参数一看”完事的东西,它更接近飞行员旁边的仪表盘,需要你熟悉每个按钮的含义——仪表盘只能在故障时节省你逐表拆开检查的时间,真正能判断故障在哪儿的永远是你对业务代码和JVM运行机制的理解是否到位。把它当成日常巡检的伙伴而不是救火队员,在问题还没发酵成事故之前就习惯性去看一眼线程状态、堆内存趋势,你踩大坑的概率会低很多。
