先讲一个每天都在发生的场景:线上服务突然告警,CPU一路飙到90%以上,你登录服务器一看,Java进程占了几个核在那疯狂运行。你准备用jstack打印线程栈,结果发现栈里全是看不懂的线程池名字,翻半天也定位不到业务代码。想加日志,可代码跑在几十台机器上,重新发布一次代价太大。这种时候,就该轮到Arthas上场了。
Arthas是目前Java生态里最常用的开源在线诊断工具,它可以做到一件很关键的事:不重启线上JVM,直接挂到运行中的Java进程上,实时查看线程、内存、类加载情况,反编译线上代码,追踪方法调用耗时,甚至动态修改日志级别。配合IDEA使用,整套排查链路能在熟悉的开发界面里完成,不需要在服务器和IDE之间反复横跳。这篇文章我会从环境准备讲起,把一套完整可落地的排查流程串起来,适合正在做Java后端、被线上问题折腾过的开发者,也适合R&D新人学一套标准排查思路。
1. 线上Java排查的痛点和Arthas的定位
1.1 传统排查手段为什么不够用
很多团队其实还停留在"出问题-看日志-猜代码"的阶段。这套流程在简单场景下勉强能用,一旦遇到复杂问题,短板会非常明显。
先说日志方案。排查问题依赖日志,但日志是提前埋好的,你没埋到某个关键位置,出了问题就只能干瞪眼。临时加日志要重新打包、发布、重启,等机器全量更新完,线上异常可能已经过去了。而且生产环境日志级别一般设在INFO,DEBUG级别的详细日志根本没打出来。你让运维改一下日志级别,可能还要走审批流程,半天就过去了。
再说jstack这类JDK自带工具。jstack只能打一次线程快照,面对几十个线程要一个个去比对,效率极低。jmap导堆快照倒是能看内存,但要把几个GB的堆文件从生产服务器拉下来分析,操作成本高,还经常出现堆文件太大、本地机器内存不够打不开的情况。更麻烦的是,jmap执行Full GC会卡顿线上服务,很多团队根本不敢在高峰期用它。
还有一个常见思维误区是"本地复现"。很多问题依赖线上特定的数据分布、并发量、机器规格,本地环境根本模拟不出来。你花两小时构造数据,最后发现根本复现不了,白忙活一场。
下面这张表可以直观对比传统方式和Arthas的差异。
| 排查手段 | 典型痛点 | Arthas的方式 |
|---|---|---|
| 看日志 | 日志没埋点,无迹可寻 | 不重启直接观察运行状态 |
| jstack | 单次快照,线程多时难定位 | thread命令筛选并高亮关键线程 |
| jmap堆分析 | 文件过大、卡顿服务 | heapdump按需触发,配合内存命令 |
| 加日志重新发布 | 耗时数小时,掩盖问题 | watchdog链路一次到位 |
| 本地复现 | 环境差异大,复现困难 | 在线直接观测真实运行现场 |
1.2 Arthas的核心能力拆解
Arthas解决问题的核心逻辑是"在线探针"。它本身是一个Java Agent,通过attach机制挂载到目标进程上,形成的是一层动态增强,不需要改动业务代码,也不需要重启进程。它挂上去之后,会打开一个交互式命令行,你敲命令,它就从运行中的JVM里捞数据给你看。
常用命令我按用途整理一下。
- dashboard:实时面板,查看全局CPU、内存、GC、线程概况,相当于给JVM装了个任务管理器。
- thread:查看线程列表、线程栈、CPU占用排序,还能检查死锁。
- jad:反编译线上真正运行的类,确认发版内容和你的预期是否一致。
- watch:观察方法调用时的入参、返回值、抛出的异常,相当于动态埋点。
- trace:追踪方法内部各子调用的耗时分布,定位性能瓶颈。
- stack:方法被调用时输出当前调用栈。
- sc/sm:查找JVM里已加载的类和方法信息。
- ognl:在目标JVM里执行表达式,查看静态字段、调用Bean方法、修改变量。
- logger:查看和修改logger级别,排查日志不输出的问题。
- tt:时空隧道,记录方法调用的现场,可以回放当时的数据。
这些命令最核心的价值,是把"猜"变成"看"。你在本地猜代码哪里有问题,和直接在线上看到真实的类内容、真实的入参返回、真实的调用耗时,解决问题的效率完全不在一个量级。
2. 环境准备:把Arthas集成进IDEA
2.1 准备工作,先把Arthas装好
在IDEA里使用Arthas,服务端其实是远端的。Arthas必须运行在目标Java进程所在的那台机器上,或者能访问到那个进程的容器里。所以第一步是准备Arthas本体。
常见的安装方式是直接下载arthas-boot.jar,这是一个引导程序。然后在目标机器上执行:
java -jar arthas-boot.jar执行后它会列出当前机器上的Java进程列表,输入序号或者直接指定PID就能attach:
java -jar arthas-boot.jar 12345还有另一种方式,下载Arthas的完整发行包,解压后里面有as.sh脚本,用法类似:
./as.sh -p 12345这里有几个容易踩的坑。第一,Arthas和目标任务必须用同一个操作系统用户启动,不然attach大概率失败。你如果用root跑的Arthas,目标是普通应用用户,就会遇到权限问题。第二,目标JVM需要能启动attach机制,有些环境做了安全加固,禁止了Java attach,这种情况要提前跟基础设施团队确认。第三,Arthas本身要求JDK版本,目标进程是Java 7的话,需要用老版本的Arthas,新版不支持,这一点后面还会细说。
2.2 方式一:IDEA Terminal 直连,我最推荐
很多人在IDEA里用Arthas,其实只用到了Terminal窗口。IDEA自带Terminal面板,可以直接在底部打开。配合远程开发插件或SSH插件,Terminal面板能直接变成远程服务器的终端会话。
具体做法是:
- 在IDEA的插件市场搜索SSH相关插件,任意你习惯的都可以,支持远程连接终端就行。
- 配置好服务器地址、用户名、密钥或密码。
- 打开IDEA Terminal面板,连接到远端服务器。
- 执行Arthas命令。
这个方式的好处是零额外配置,不引入任何新的操作习惯。你在IDEA里写代码,遇到线上问题了,切到Terminal连上服务器,Arthas的彩色输出、关键字搜索、日志联动都能直接在IDEA里做到。而且IDEA的Terminal支持多Tab,你可以开一个窗口连服务器,开另一个窗口看本地代码,对照起来非常方便。
2.3 方式二:External Tools 一键Attach
如果你觉得每次敲命令都麻烦,还有一个更符合"结合IDEA"的操作——利用IDEA的External Tools功能。它允许你把任意外部命令注册成IDEA菜单里的一个工具项,点击就能执行。
先准备一个attach脚本,比如叫arthas-attach.sh:
#!/bin/bash # 用法: arthas-attach.sh <服务器> <PID> SERVER=$1 PID=$2 ssh ${SERVER} "java -jar /opt/arthas/arthas-boot.jar ${PID}"然后在IDEA里依次打开:Settings -> Tools -> External Tools,新增一个工具:
- Name填Arthas Attach
- Program填脚本路径,比如/Users/me/bin/arthas-attach.sh
- Arguments填$Prompt$,这样运行时IDEA会弹窗让你输入服务器和PID
- Working directory填当前项目目录
配置完成之后,你点击Tools -> External Tools -> Arthas Attach,输入服务器地址和PID,IDEA会自动打开Terminal并启动Arthas。
我对这种方式的评价是:适合固定使用同一套部署环境的团队。你把脚本放在公共脚本库,团队成员拉下来之后,排查线上问题的入口就统一了。不过它本质上还是帮你少敲几行命令,核心能力没有变化。
2.4 方式三:IDEA插件方式
IDEA的插件市场里也有专门的Arthas插件。这类插件通常提供一个工具窗口,配置服务器列表和认证信息后,可以直接在IDEA面板里看到目标服务器上的Java进程列表,选中进程点一下就能attach进Arthas。
插件方式的优点是图形化,适合不熟悉命令行的人。但它的配置项往往比脚本方式复杂,而且插件本质上是包了一层简洁界面,遇到Arthas具体命令的调试还是要回到命令行交互模式。我更倾向于把它理解成一个"快捷启动器",而不是Arthas的全部。
表格对比这三种方式,方便你结合自己的团队情况选。
| 集成方式 | 上手成本 | 适用场景 | 主要局限 |
|---|---|---|---|
| Terminal直连 | 最低 | 单人临时排查,日常使用 | 每次手动敲连接命令 |
| External Tools | 中 | 团队统一操作入口 | 需要维护脚本参数 |
| IDEA插件 | 中高 | 喜欢图形界面的使用者 | 配置复杂,依赖插件维护状态 |
3. 完整实战流程:从一个告警到定位根因
3.1 第一步:dashboard看总览,先别急着钻代码
假设你现在收到告警,说某服务的接口P99延迟暴涨,同时CPU利用率持续在90%以上。如果你直接去翻日志,大概率会迷失在大量异常堆栈里。我的习惯是先上dashboard,花一分钟掌握全局状态。
进入Arthas交互界面后,敲:
dashboard输出会展示几块信息:当前JVM的CPU占用率、内存使用情况、垃圾收集次数和耗时、线程总数量。最关键的是线程部分,它会列出占用CPU最高的几个线程,并显示线程的状态和名称。
这一步要看的信息点有这么几个:
- 线程总数是否异常,是不是有大量线程卡在某个状态里出不来。
- GC频率是否过高,如果每秒都在Young GC甚至Full GC,那CPU飙高的根源大概率是内存分配压力过大。
- 占用CPU最高的线程叫什么名字,是业务自定义线程池的工作线程,还是JVM内部的GC线程,这直接决定了你下一步去哪排查。
打个比方,dashboard相当于你去医院先做个全身检查,拿到一份体检报告。你不知道具体哪生病,但报告会告诉你大致方向是消化科还是神经内科。通常我看完dashboard,心里基本有数了。
3.2 第二步:thread定位问题线程,看栈找真凶
如果dashboard显示CPU高的是某个业务线程,下一步就是用thread命令精确锁定。
执行:
thread -n 3这个命令会显示CPU占用前三的线程,包括线程ID、名称、CPU占比和堆栈摘要。这里的堆栈摘要非常关键,它直接反映了这个线程在那一瞬间正在执行什么代码。
假设输出显示一个名为biz-thread-27的线程,CPU占用90%,栈顶指向我们自己应用的一个类的方法。这时候再单独看这个线程的完整栈:
thread 27注意这里的27是线程ID,不是进程PID。完整栈会告诉你调用链路,比如它从某个MQ消费入口进来,经过了一长串处理逻辑。栈顶停留在哪,热点就在哪。如果是容器或框架的线程池,栈顶往往是指向JDK内部的类,这时候要看栈中间段,找到进入当前业务类的入口方法。
我遇到过不少这种情况:线程栈看着是JVM里的MethodHandle调用,实际上是因为某个框架在做动态代理,把业务代码包了一层。所以不要看到JDK类就跳过,多看几层。
如果还怀疑是死锁问题,可以加一条参数:
thread -b这条命令会直接检测并输出死锁信息。出现死锁时,它会列出互相等待的线程对,以及各自的持锁信息。这个比手动对比线程栈快得多。
3.3 第三步:trace追踪调用链,定位耗时瓶颈
线上接口慢,光看线程栈还不够,因为栈能告诉你在哪个方法里,但看不出哪个子调用最耗时。尤其是那种循环调用远程接口、多次查数据库的方法,整体慢,但不知道慢在哪一段。这时候就要用trace命令。
直接追踪接口的入口方法:
trace com.example.OrderService queryOrderInfo -n 3参数含义是:追踪OrderService类的queryOrderInfo方法,总共采样3次。Arthas会截获这个方法的调用,输出它内部所有子调用的耗时分布。输出大致长这样:
`---ts=2024-01-15 10:23:45;thread_name=biz-thread-27;id=12;is_expensive=true;invocation_id=233 `---[1000.25ms] com.example.OrderService#queryOrderInfo() +---[800.1ms] com.example.OrderRepository#getFromDB() +---[50.2ms] com.example.MemberService#getMemberInfo() `---[20.1ms] com.example.StockClient#getStockInfo()一眼就能看出,查询数据库的getFromDB()方法占了800毫秒,其他加起来才几十毫秒。那下一步就很明确了:要么这条SQL本身慢,要么数据库连接池拿不到连接在等待。
如果还想往深处钻,可以继续trace这个子方法:
trace com.example.OrderRepository getFromDB -n 3trace命令高阶用法是条件过滤,比如只追踪某个耗时超过阈值的调用:
trace com.example.OrderService queryOrderInfo 'cost>500'这样即方便,又能避免刷屏。实际排查时,我会先用不带条件的trace确认瓶颈的大方向,再用条件过滤的方式做第二轮精准分析。
3.4 第四步:watch观察入参、返回和异常
有时候问题不在性能,而在逻辑。比如当前端反馈某个订单状态错乱,你怀疑是这边方法收到了脏数据。加日志要发版,又来不及,这时候watch命令直接上。
watch com.example.OrderService queryOrderInfo "{params, returnObj, throwExp}" -x 3 -n 5这条命令的含义是:观察queryOrderInfo方法的入参、返回对象和抛出的异常,展开深度为3层,采样5次。执行之后,你发一个测试请求,Arthas就会实时把完整的入参和返回打印出来。
我实际用下来,watch有几个特别有用的场景:
第一,确认入参是否为空或者字段取值异常。有时候是前端传了空字符串,有时候是网关把参数搞丢了,你在服务端怎么查都查不到问题,看入参一眼就明白了。
第二,确认返回值和数据库里的数据是否一致。如果数据在数据库里是对的,接口返回是错的,那问题就锁在方法内部的字段映射上,再去看代码就好办多了。
第三,看异常。如果方法抛出异常,而这个异常被上层吞掉了,日志里一直神秘失踪,watch可以直接抓到throwExp字段。
watch还支持条件表达式,例如只看某个订单号的调用:
watch com.example.OrderService queryOrderInfo "{params, returnObj}" 'params[0].orderId.equals("A123")'这个功能在并发环境下特别有用。线上很多请求同时在跑,你不加条件过滤,输出会非常杂乱。加上订单号过滤之后,只看那一条,定位准确又高效。
3.5 第五步:jad反编译,验证线上代码版本
还有一种特别常见的问题:本地代码改了,也上线了,但线上行为完全没有变化。这种情况十有八九是发版内容和你以为的不一致,比如包没有更新成功、灰度路由没切到你这个实例、或者旧节点一直没被换下来。
Arthas的jad命令可以直接反编译JVM中已加载的类:
jad com.example.OrderService执行之后,控制台会输出现场这个类真正被加载的源码,包括方法实现和注释。你只需要看关键方法的代码,就能确认线上跑的是不是你期望的版本。
比如你在本地重构了queryOrderInfo方法,把旧的查询逻辑改成了缓存+异步的方式。反编译线上类之后,如果看到的还是旧的数据库直查代码,就知道当前节点没有更新上。如果方法里多了一个你在代码里写过的特有日志,就说明这个版本确实已经生效。
不过需要提醒一句:jad输出的是反编译结果,和原始源码之间存在可读性差异,变量名可能变成var1、var2这种形式,注释全都消失。不要拿它去审计逻辑细节,它更适合用来判断版本和查看关键分支是否存在。
3.6 附加操作:动态修改日志级别和收尾
排查过程中经常要临时把日志级别从INFO调到DEBUG,观察一段时间后再调回去。传统做法是改配置重启,Arthas可以直接操作:
logger --name ROOT --level DEBUG执行完这条命令,日志瞬间铺开。排查完成后,记得再调回INFO:
logger --name ROOT --level INFO这里我要特别强调恢复现场。很多人查完问题就退出Arthas,日志级别忘改了,第二天日志文件直接膨胀几十GB,这就是我踩过的坑。改过什么,退出之前一定要原样改回来。
退出Arthas的命令是:
stop或者:
exit两者区别在于,exit只是退出当前客户端连接,目标进程上可能还有残留的trace/watch任务;stop会完整停止Arthas,卸载增强逻辑,释放资源。收工之前,稳妥起见用stop。
4. 常见问题与避坑技巧
4.1 高频问题速查表
使用Arthas过程中,有一批问题出现的频率特别高,我先整理成表格。
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| attach时说找不到Java进程 | 容器环境PID隔离、进入错误容器 | 用nsenter或docker exec进入目标容器,在容器内执行Arthas |
| 报Unsupported class file version | 目标JVM版本和Arthas不匹配 | 换成对应旧版Arthas,或升级目标JDK版本 |
| watch/trace没有输出 | 方法签名不对、调用未触发 | 先sc确认类已加载,sm确认方法签名 |
| 反编译类不存在 | 类由自定义ClassLoader延迟加载 | 先触发一次调用,再执行jad |
| trace显示overhead高 | 采样条件过宽,方法调用频率太高 | 加条件过滤,用-n限制次数,或改用watch走读现场 |
| 服务器无法下载Arthas相关文件 | 外网受限,依赖Maven中心仓库 | 提前手工下载arthas-boot.jar,内网全量分发 |
| 提示进程无权限attach | 用户不一致或JVM安全限制 | 使用和目标进程相同的用户,检查安全加固白名单 |
4.2 我实战中的几个习惯
Arthas用久了之后,我养成了几个固定习惯,分享出来供参考。
第一,排查问题前先确认"线上代码是不是最新版"。我见过太多人拿着旧版本代码在线上定位问题,费了半天劲,最后发现代码根本不是这个版本的。先jad一遍,五分钟就能排除这个变量。
第二,尽量用条件过滤和限制次数。watch一个高频方法,如果不加-n参数,Arthas会一直打印下去,不仅刷屏,还有额外性能开销。我一般固定使用-n 5或者-n 10,拿到足够现场数据就停。
第三,把常用排查命令整理成脚本模板。公司内部可以做一个包含标准排查清单的文档,比如第一轮跑dashboard、thread -n 3,第二轮按接口名trace。这样即使一个新人,照着清单也能把初步信息收集齐全,不用临时翻文档。
还有个细节,Arthas本身有Web Console能力,可以通过浏览器访问同一套命令行。这对"服务器不能直连"的场景比较实用,比如只能通过跳板机登录的环境。不过这个功能需要配置tunnel服务端和客户端,属于进阶用法,用不到的人可以暂时跳过。
4.3 关于线上安全使用的一点建议
Arthas是一个诊断利器,但它也确实具备很强的操作能力。比如ognl表达式可以直接修改线上对象属性、调用任意方法,这相当于你给线上程序开了一个随时可以执行代码的后门。生产环境使用Arthas必须有纪律:
- 只能在授权范围内排查,避免在核心生产环境执行修改类操作。
- 实时改动(比如改日志级别)尽量避开业务高峰,并做好记录。
- 排查任务完成后,务必检查有没有残留的watch、trace监听,统一stop掉。
- 团队内部统一使用方式,不要每个人各敲各的,避免互相干扰。
我倾向于把Arthas当成一个"只读优先"的工具。平时多用它看线程、看栈、看调用耗时、看反编译,这些是安全的观测动作。涉及修改的操作,确认必要且可控,再谨慎执行,用完立即恢复。
从实际的排查效率看,Arthas给我最深的感受是,它把"线上程序不可见"这件事基本解决了。以前排查线上问题,像是隔着门听里面动静,现在等于把门打开了,直接走进去看。配合IDEA使用之后,剩下的操作习惯问题都是锦上添花。真心建议每位Java开发者都在本地演练一遍这些命令,真正上线出问题时,你会感谢自己提前练过手。