线上应用突然CPU飙到90%,接口超时告警一条接一条,代码看起来却毫无头绪——这是每个后端开发者都经历过的噩梦。Arthas就是为解决这类问题而生的Java诊断工具,它能在不重启应用、不修改代码的前提下,直接钻进运行中的JVM内部,查看方法调用参数、返回值、异常堆栈、线程状态甚至热更新代码。很早以前我在某电商团队排查一个诡异的内存泄漏问题时接触到了它,从此便离不开了。这篇文章从一个实际使用者的角度,把Arthas的核心功能、实际操作和避坑经验一次讲清,适合所有写Java的后端开发者、运维和性能排查人员阅读。
1. 为什么你需要Arthas而不是一直在重启
1.1 线上排障的常规痛点
传统的线上问题定位流程相当磨人。先看监控面板确认异常指标,然后翻日志找堆栈,可日志打得不够全的话,往往只能看到调用链的入口和出口,中间发生了什么完全是个黑盒。接下来是本地复现,可很多问题的触发条件依赖特定的数据状态、并发时机或缓存命中情况,本地环境根本复现不出来。
在这种时候,常见的兜底方案是加日志重新发布。加日志、发版、复现、看日志,运气好一轮就能定位,运气不好要来回折腾好几次。更糟的是加日志本身会改变运行状态,某些隐蔽的并发问题可能加了日志就不复现了,白白浪费时间。
Arthas的思路完全不一样。它直接连接到正在运行的Java进程上,不修改任何业务代码,不需要停服,在问题发生的现场就能拿到所需的全部信息。这相当于给运行中的应用装了一个内窥镜,可以直接看到JVM内部的一举一动。
1.2 Arthas能做什么
Arthas是某个大型互联网公司开源的一个Java诊断工具,核心能力是实时的线上问题定位。列举几个高频使用场景你就明白了:
- 接口突然变慢,想知道慢在哪个方法、哪个环节。用trace命令一条链路点进去看,每层方法的耗时清清楚楚。
- 方法返回结果和预期不一致,但日志里没有打印参数和返回值。用watch命令去观察某个方法,参数、返回值、异常全部实时获取。
- CPU飙升到100%,核心线程池被打满。用dashboard和thread命令直接看哪个线程在干什么、卡在哪个堆栈。
- 线上有一段逻辑有问题,代码审查发现就是一行判断写反了。用jad反编译确认,然后mc+redefine直接在运行中修复。
- 想知道某个静态字段当前的值、某个Spring Bean的属性状态,甚至想临时改一个配置项。用ognl表达式一行搞定。
这些场景全部不需要重启进程,不需要改代码重新发版。对线上服务来说,越少变化越安全,Arthas正好切中了这个诉求。
1.3 和同类工具的对比
很多人会问,jstack、jmap、jstat这些JDK自带的工具不是也能看线程和堆内存吗?确实能,但体验差距很大。jstack只能拿到那一刻的线程快照,看不到方法的入参和返回值,看不到调用链的耗时分布;jmap在大型堆上执行会触发Full GC,线上用起来有风险。Arthas则把这些能力集成成了交互式的命令工具,并且额外提供了反编译、热更新、方法观测等实战性极强的功能。
Btrace是另一个同类工具,通过编写脚本注入到目标JVM里做字节码增强。但Btrace的约束比较多,脚本语法受限,对安全的控制也严格,学习成本高。Arthas的入门门槛低得多,都是从命令行直接敲命令,没有代码门槛。
提示:Arthas不是一个永久驻留的服务,它相当于一个瘦客户端,按需连接到目标进程上。排查完问题可以随时退出,不影响业务应用的运行。
2. 安装与核心操作流程
2.1 快速安装与启动
Arthas的安装方式非常简单,一条命令就能完成。在服务器上下载启动脚本,然后运行它:
# 下载Arthas启动脚本 curl -O https://arthas.aliyun.com/arthas-boot.jar # 启动Arthas(会进入交互式命令界面) java -jar arthas-boot.jar启动后,工具会列出当前机器上所有运行中的Java进程,输入序号即可选择要诊断的目标进程。这里的原理是通过attach机制连接目标JVM,而不是重新启动一个新的Java进程,所以对目标应用几乎没有侵入性。
如果服务器无法访问外网,可以在本地开发机下载好arthas-boot.jar再传上去。还有一种方式是直接在pom.xml里加上依赖,用启动参数的方式在应用启动时自动attach,适合对稳定性和便利性要求都高的场景。
启动完成后会看到一个类似[arthas@pid]$的命令提示符,这就进入了交互模式。在这个终端里输入命令,按回车执行,退出时输入quit或exit即可。
2.2 全局命令速览
进入交互模式后,有一个命令一定要先记住——help。它会列出所有支持的命令及其简要说明。Arthas的命令设计得很有层次感,大体可以分为几类:
- 基础信息类:dashboard查看整体面板,thread查看线程,jvm查看JVM信息,sysprop和sysenv查看系统属性和环境变量。
- 方法观测类:watch、trace、stack、tt,这些是排查业务逻辑问题的核心命令。
- 字节码相关:jad反编译、mc内存编译、redefine热更新、classloader查看类加载器。
- 表达式与工具类:ognl执行表达式,sc和sm检索类和方法信息,logger管理日志级别。
- 异步任务类:async后台执行耗时命令,job管理任务状态。
刚上手时不用记太多,先把dashboard、thread、watch、trace、jad这五个命令练熟,就能覆盖大多数排查场景。
2.3 连接非默认进程的细节
实际使用中经常遇到多实例部署的情况,机器上有十来个Java进程,用java -jar arthas-boot.jar列出来一长串,容易选错。这里有几种精准定位的做法:
# 通过进程ID直接连接 java -jar arthas-boot.jar 12345 # 列出进程时只显示包含特定关键字的进程 java -jar arthas-boot.jar -l # 通过关键字匹配进程 java -jar arthas-boot.jar --select-order muservice还有更直接的方式,用ps命令先找到进程ID,然后用java -jar arthas-boot.jar PID连进去。如果目标进程是以特定用户启动的,要注意当前用户权限问题,否则attach时会报错,这种情况需要切换到对应的系统用户再执行。
3. 核心观测命令的实战拆解
3.1 dashboard与thread:快速定位资源瓶颈
接到CPU飙高的告警后,我通常第一件事就是输入dashboard。这个命令会以面板的方式展示目标JVM的整体状况,包括线程总数、内存使用量、垃圾回收器运行情况,以及CPU占用最高的线程排名。
$ dashboard ID NAME GROUP PRIORITY STATE CPU% MEMORY 42 "http-nio-8080-exec-11" main 5 TIMED_WAITING 31.2 3.1 17 "GC task thread#0 (Parallel)" system 10 RUNNABLE 25.8 0.2 ...从面板里一眼就能看到哪个线程在吃CPU。记下线程ID后,用thread命令查看这个线程的详细堆栈:
thread 42这时会打印出该线程从入口到当前执行位置的完整调用栈,问题往往就藏在最深处的那几个帧里。可能是一个循环里在反复做耗时的字符串拼接,可能是正则表达式的回溯陷阱,也可能是某个同步方法被长时间持有锁。
还有一个高频用法是thread -n 3,直接打印CPU占用最高的3个线程的堆栈,不用先看dashboard再手动翻ID,效率更高。
注意:thread命令展示的是线程当时的堆栈快照,如果问题线程的堆栈变化很快,建议结合
thread -n连续采集几次,观察堆栈是否稳定在同一处。如果每次抓到的堆栈都不一样,可能不是某个方法本身的性能问题,而是线程频繁发生上下文切换或争夺锁导致的。
3.2 watch:观察方法的入参、返回值和异常
当问题表现为“某个接口返回的数据不对”,而日志里又没打印相关变量的值时,watch命令是最直接的解法。它的思路是在指定方法上注入一个观测切面,拦截每一次方法调用,把入参、返回值、异常和调用耗时打出来。
watch com.example.service.OrderService createOrder '{params, returnObj, throwExp}' -x 2这个命令里值得展开讲的是几个关键点。类名和方法名必须写全限定名,支持通配符。表达式部分用OGNL语法,可以访问params、returnObj、throwExp、target、clazz等内置变量,也可以从这些变量里继续取字段,比如returnObj.getItems()。-x参数控制展开的深度,默认是1,如果返回结果是嵌套的对象,展开深度不够会显示为@ArrayList[3]而不是具体内容,这时候调大-x即可。
-n参数可以限定只观测前几次调用,避免观测结果刷屏。配合-f可以只关注方法返回的时候,不关注进入时。实际排查中,我最常用的是下面这条组合:
watch com.example.service.OrderService createOrder '{params, returnObj}' -x 2 -n 5执行后调用一次对应接口,就能看到五次调用里面每次的入参和返回结果。哪个参数不对、哪次返回异常,一目了然。
3.3 trace:追踪方法调用链路上的耗时分布
watch能看某一次调用的参数和返回,但接口整体变慢的时候,需要的是知道耗时到底花在哪个子方法上。这就是trace命令的主场。
trace com.example.controller.OrderController createOrder '#cost > 200'执行后会打印出这个方法内部所有子调用的耗时分布。观察点在于不是平均耗时而是总耗时,如果整条链路耗时超过200毫秒,就会把完整的调用树和每层耗时输出出来。子方法再嵌套的子方法也能一层层展开,多级调用链路能看得很深。
有个容易踩的坑要提前说明:trace命令默认只追踪直接调用的一层子方法,如果链路中有异步调用、线程池提交、或者中间隔了动态代理生成的类,子方法不会自动出现在trace结果里。遇到这种情况需要手动展开,用-e参数在trace结果里继续探测更深的调用,或者换用watch去观察更底层的方法。
trace还有一个常用配件是--skipJDKMethod false,默认情况下JDK自带的方法调用会被折叠,如果怀疑性能瓶颈在某个JDK底层方法上,需要把它展开来看。
3.4 stack:查看方法被调用的完整来源
有时候你不仅想知道某个方法当前的执行情况,还想知道它是被谁调过来的。stack命令回答的是“谁调用了我”这个问题。
stack com.example.service.InventoryService deductStock执行后,任何一次对deductStock方法的调用都会在观测端打印出完整的调用来源栈。这在排查“某个方法明明没人在用,但总是被执行”的场景下特别好使。比如实际排查中遇到过定时任务和接口请求同时触发同一个方法,通过stack就能确认调用方和触发频率。
stack与watch、trace配合使用可以拼出一个完整的事件视图:谁调用、什么参数、内部耗时、返回结果,四块信息凑齐,大部分业务逻辑问题都能定位到根因。
4. 字节码级操作的实战与应用边界
4.1 jad:反编译确认线上代码的真实逻辑
线上运行的代码往往和本地分支不完全一致,可能是发版漏了文件、构建时打了旧包、又或者是多个版本叠加后的产物。直接看代码猜逻辑不可靠,最稳妥的方式是反编译线上正在跑的那个类。
jad com.example.service.PriceService执行后,Arthas会从JVM中读取该类的字节码,反编译成可读性很高的Java源码打印出来。为了确定线上这个类是否真的执行了某段逻辑,只需看一下反编译结果即可。
如果担心反编译出的代码因为内部类、Lambda等原因不够准确,可以加上--source-only参数只输出源码部分,便于和本地代码对比。反编译结果还能整体保存为文件,命令是jad com.example.service.PriceService > /tmp/PriceService.java。
4.2 mc和redefine:在运行中修复问题代码
在一些必须立即止血的场景下,重新发版流程太慢,Arthas提供了一套“现场改代码”的能力,原理是修改内存中的字节码。
基本流程分三步。第一步用jad反编译出类源码,存成文件。第二步在本地修改文件中的逻辑,用mc命令在内存里编译修改后的代码,生成新的字节码。第三步用redefine把新字节码装载到JVM中,替换掉老的实现。
# 反编译类 jad --source-only com.example.service.PriceService > /tmp/PriceService.java # 编辑文件,修改逻辑 vim /tmp/PriceService.java # 内存编译 mc /tmp/PriceService.java -d /tmp/out # 热更新 redefine /tmp/out/com/example/service/PriceService.class需要特别注意的地方:redefine不会改变类的结构,不能增加方法、不能删除字段、不能改变方法签名,只能修改方法体内部的逻辑。这一点对于临时止血来说通常够用,但要记住热更新后的代码在下次应用重启后会丢失,务必及时把修复同步到正式分支里发版。
4.3 ognl:用表达式直达对象内部
有时候你想知道一个Spring Bean的属性当前是什么状态、某个静态Map里缓存了哪些数据、或者直接修改一个配置项的值。这些需求都可以用ognl表达式完成,相当于在JVM里执行一段代码片段。
# 查看某个静态字段的值 ognl '@com.example.config.AppConfig@MAX_RETRY' # 调用某个bean的方法并拿到返回值 ognl '@com.example.service.UserService@getInstance().getUserById(1)' # 修改一个静态字段的值 ognl '@com.example.config.AppConfig@MAX_RETRY=5'ognl表达式的能力很强,可以访问静态字段和方法、访问Spring容器里的Bean、调用实例方法、甚至可以执行一些简单逻辑。官方文档里也有更多细节,比如配合-c参数指定classloader,解决类找不到的问题。
注意:ognl能读取和修改jvm内部数据,等同于拥有对目标进程的控制权,在生产环境操作时务必小心,不要误改了影响全局的配置。
5. 三个真实排查案例的完整复盘
5.1 案例一:接口P99耗时飙高,链路却看不出异常
曾经在某系统上遇到一个诡异问题,用户查询接口的P99耗时从50ms涨到2秒,但接口本身的逻辑非常简单,数据库查询也都走了索引。我先用dashboard确认了CPU和内存没有异常,再用trace命令观测接口内部调用,结果所有子方法的耗时加起来也不超过60ms。
这里就凸显出trace的一个盲区:它只能追踪方法内的调用,如果耗时发生在方法外的中间件环节、或者是在进入到这个类之前就已经产生了大量等待,trace结果里看不到。进一步用watch观测这个接口的入口方法时,发现了一个细节:进入方法的时间戳比请求到达Controller的时间戳晚了将近1秒。
顺着这个方向排查,最后定位到是网关层到应用服务器之间的连接池满了,请求在Tomcat的accept队列里排队。这类问题的根因不在业务代码里,而在连接池配置和容器参数上。这个案例给我的经验是:链路追踪只能证明“业务代码内部没有瓶颈”,不能证明“整体没有瓶颈”,耗时分布要结合网络、容器、队列一起看。
5.2 案例二:内存持续增长但压测不OOM
某次在另一个服务上排查内存问题,现象是堆内存占用的折线图“锯齿状”但总体向上偏移,每次GC后回收不完全,可用内存越来越少。用jmap dump堆文件分析不是什么好选择——大堆dump不仅耗时,还会触发Full GC影响线上请求。
Arthas登场后,我首先用dashboard观察内存和GC情况,然后用memory命令查看各内存区域的使用量,发现Metaspace区域增长显著。接着用classloader命令查看所有类加载器的加载数量,定位到某个动态生成类的加载器数量持续增加,每次操作都创建了一个新的类加载器,加载的类永远无法卸载。
这类问题的典型场景是频繁使用动态代理、CGLIB或者Groovy脚本编译。确认根因后,修复方向就很明确:缓存类加载器,避免重复创建。整个过程没有dump堆,没有重启应用,两个时钟内完成定位。
5.3 案例三:某方法偶发地返回错误结果
偶发性问题最难排查,因为复现概率低,日志里也不一定有线索。某团队在排查一个偶发的订单金额计算错误问题时,日志显示金额有时对有时错,但触发条件不清楚。
用watch命令观测计算金额的方法,设置条件表达式只对“结果金额小于0”的情况打印入参和调用来源:
watch com.example.service.PricingService calculate '{params, returnObj}' 'returnObj < 0' -n 20这个条件观测比全量观测更高效,不刷屏、不干扰正常调用。由于系统性重试了几次后成功捕获到异常样本,发现入参中的折扣字段出现了负数,顺藤摸瓜定位到上游数据同步任务在特殊情况下会写入负数折扣。这么隐蔽的问题,用普通日志加复现的方式几乎没有可能快速找到。
6. 生产环境使用Arthas的安全握与常见问题
6.1 权限控制与操作规范
Arthas功能强大,意味着风险也很高。它能查看和修改JVM内部数据,这种能力在生产上必须要控制使用范围。我的建议是遵守几个基本原则:
- 只授予必要的开发、运维人员使用权限,在堡垒机或跳板机上做好操作审计。
- 操作前确认目标进程无误,特别是在同一台机器上有多个Java进程的情况下。
- redefine和ognl写操作属于高危动作,团队内部应有审批流程,不要随意使用。
- 排查完成后记得执行shutdown退出Arthas,避免常驻进程占用资源。
- 不在核心交易链路和监管要求严格的场景下使用redefine,除非是极端情况需要止血。
6.2 常见问题速查表
| 问题现象 | 可能原因 | 解决方式 |
|---|---|---|
| 启动时提示无法attach到进程 | 当前系统用户和目标进程用户不一致 | 切换到目标进程的启动用户再执行 |
| watch结果不打印 | 方法没有被调用,或类匹配错误 | 先用sc查看类的全限定名,确认方法签名 |
| trace看不到子方法耗时 | JDK方法被折叠,或代理类导致链路断裂 | 加--skipJDKMethod false,或手动展开代理类 |
| 反编译结果和本地代码不一致 | 线上版本和本地分支确实不同 | 以反编译结果为准,确认线上真实逻辑 |
| ognl报ClassNotFoundException | 目标类由非默认classloader加载 | 用-c参数指定正确的classloader |
| redefine报类结构变更异常 | 修改了方法签名或类结构 | redefine只能修改方法体,不能改变类结构 |
| 高版本JDK下attach报错 | JDK版本兼容性问题 | 升级Arthas到最新版本,或使用对应版本分支 |
6.3 一些值得分享的操作习惯
实际用了这么多年,有几个小习惯很值得养成。第一个是善用-n参数限制输出量,不管是watch还是trace,不要让它无限跑下去,定义好观测次数,结果够了就停,否则输出过多反而影响目标应用性能,还容易让现场信息淹没在刷屏中。
第二个是合理使用async后台执行。trace命令在调用量不大的接口上可能要等很久才能捕捉到一次调用,这会一直占用Arthas前端会话。用async命令把trace放到后台跑,先去做其他排查工作,过一会儿用job查看结果,效率高很多。
第三个是在Arthas里写复杂的条件表达式前,先用简单表达式验证一下语法。比如不确定某个字段怎么取,先用ognl探一下对象结构,再把它写进watch的条件里,能避免很多“表达式不对啥都观测不到”的无效等待。
7. 结语:Arthas解决的不只是排查效率,更是排障思维
Arthas最打动我的地方在于它改变了排查线上问题的思维方式。以前是“猜问题、加日志、发版、等结果”的循环,现在变成了“看现场、拿数据、确认根因”的直接路径。排查问题的时间从小时级压缩到分钟级,这不仅仅是效率提升,更意味着错了的问题可以追得回去、偶发的问题可以有条件地复现出来。
我个人在实际使用中体会最深的一点是:不要等线上出了问题才打开Arthas。在日常联调环境、压测环境里多跑几遍这些命令,把线上服务的关键方法提前观察一遍,了解它们的调用链路和耗时基线,真到出问题那天你会比任何人都更快定位到异常。工具本身不难学,难的是形成“运行中的代码也是可以被观察和验证的”这种意识。Arthas给了我们这双“眼睛”,只要你愿意在平时多用它看看系统,关键时刻它就不会让你失望。