☰
Arthas实战:Java线上CPU飙升与线程问题排查指南
2026/10/20 17:13:13 网站建设 项目流程

凌晨一点,线上的Java应用突然CPU报警,P99从几百毫秒涨到好几秒。你第一反应是登服务器看日志,结果发现关键方法上根本没打全参数;你想用jstack抓一下线程栈,看到了堆栈但不知道那个线程在业务上到底执行了什么;咬咬牙加日志重新发布,几分钟后问题复现,可日志依旧零散,根因还是一头雾水。这种状态下,我脑子里第一个调出来的工具永远是Arthas。它不是那种“重启大法”的安慰剂,而是一把能直接伸进正在运行的JVM里做现场勘探、做临时止血、甚至反编译看代码的“手术刀”。

Arthas是一个开源Java诊断工具,主要解决线上问题难以复现、不能随意重启、缺少观测点这类痛点。它能直接attach到目标Java进程上,用一条条命令查看类加载、方法调用、参数、返回值、异常、线程栈、GC状态,还能在不重启的情况下动态修改部分代码行为。无论你是后端开发、运维还是SRE,只要你的服务是JVM系的,这套工具都值得放进日常排查的常备清单里。

1. 为什么线上排查首选Arthas:从“重启大法”到“在线手术”

1.1 传统排查三板斧的痛点

做Java的人应该都有过这种经历:线上出问题,能用的招数基本就是三板斧。

第一斧看日志。日志打得全,你好判断;日志没打在关键路径上,你就只能靠猜。更尴尬的是,线上很多问题都出现在你没打日志的那几行代码里,比如某个字段在极端情况下变成了null,某个外部依赖偶发超时,某个队列长度溢出。日志里什么都看不到,重启之后现场全没了。

第二斧重启服务。重启确实能快速止血,但如果你不知道根因,下一次报警只会来得更猛。而且重启会清空堆内存、线程状态、连接池状态,很多故障在重启后就不再复现,你连抓现场的机会都没有。这就像家里漏水,你把总闸关了,水是不漏了,但漏水点在哪你永远不知道。

第三斧加日志重发。这个思路靠谱,但要经历改代码、走发布流程、重新上线,最快也要十几分钟。如果问题是偶发性的,新日志未必能立刻抓到现场。而且加日志本身也可能改变时序,导致问题更难复现。

更精细一点的工程师会用jstack、jmap、jstat这些JDK自带工具。它们能看线程堆栈、堆内存、GC情况,但看不了“某个方法被调用了多少次”“入参到底是什么”“返回值和预期差在哪”“子调用各花了多少毫秒”。这些信息才是定位业务逻辑根因的关键。

1.2 Arthas能“贴”着进程看到什么

Arthas做的事情,本质上是把JDK自带工具的“静态快照”升级成“动态探针”。你可以理解成给运行的Java进程安装了一个后视镜和一组传感器,不用停车就能检查发动机内部状态。

核心能力我列一下:

  • dashboard:实时查看JVM整体状态,包括CPU、内存、GC、线程数、类加载数量。
  • thread:查看线程状态和堆栈,还能按CPU占用排序,直接锁定最繁忙的线程。
  • jad:反编译已经加载的类,即使线上代码和本地不一致,也能看到JVM里真正在跑什么。
  • watch:观测方法的入参、返回值、抛出异常,支持按条件过滤。
  • trace:追踪方法内部调用路径,打印每个子调用的耗时和堆栈。
  • monitor:统计某个方法在一段时间内的调用次数、成功失败次数、平均耗时。
  • sc/sm:查看已加载类和类中的方法信息。
  • ognl:在运行期执行OGNL表达式,读取或修改对象的字段。
  • redefine:用编译好的class文件替换正在运行的类的字节码。

这些能力叠加起来,基本覆盖了线上排查的绝大多数场景。最香的是,这些操作全程不需要重启进程,也不需要提前埋点。

1.3 原理先弄明白,才不会被它的行为吓到

我第一次用Arthas也很忐忑,怕一条命令把线上进程搞挂了。它的原理其实是这样:Arthas作为一个Java进程,通过JDK提供的attach机制“附”到目标Java进程上,然后利用字节码增强技术在目标类的方法入口和出口插入探针逻辑。

你写的业务代码不会被改动,Arthas只是在运行时给Class文件做了代理或增强,相当于在你的方法周围包了一层“观察膜”。命令执行完,如果你退出Arthas,这些探针通常会随着类重新加载或会话结束被清理掉,不会永久污染你的业务代码。

但要记住:redefine这类命令是真的会用新字节码替换当前类,所以要格外谨慎。清楚原理之后,你就知道哪些操作可以大胆试,哪些操作要像做手术一样确认再确认。

2. 安装Arthas之前必须搞清楚的几个前提

2.1 环境要求:别等到attach失败才看官方文档

Arthas的安装本身很轻量,但环境不满足,装了也白装。先确认三件事。

第一,操作系统。主流Linux、macOS、Windows都能跑,但Windows下终端体验比较别扭,很多命令有转义问题,建议先在Linux服务器上实操。

第二,JDK版本。Arthas需要Java环境来运行,同时要能attach到目标Java进程。目标进程最好也是JDK,而不是裁剪过的JRE,否则某些诊断功能会因为缺少模块而不可用。

第三,权限。attach操作对操作系统用户有限制。你想诊断Java进程,就必须有该进程的系统权限。很多生产环境用普通运维账号跑Java,你用另一个账号执行Arthas,大概率会报Permission denied或无法attach。

容器环境更特殊:在Docker容器里,你要确保容器有SYS_PTRACE之类的capability,否则Arthas连不上。Kubernetes pod里一般需要进入容器执行,同样要注意镜像是否带完整JDK。

2.2 安装方式:快速启动和离线安装两种都要会

最常见的安装方式是用arthas-boot.jar这个引导包。它不是一个完整的Arthas,更像一个下载器。下载后执行:

curl -O https://your-mirror/arthas-boot.jar java -jar arthas-boot.jar

第一次运行时,它会自动从配置的远程仓库拉取Arthas的完整依赖包,然后列出当前机器上的Java进程让你选择。

但生产环境通常不能随便访问外网。所以更靠谱的做法是直接下载完整包,解压到服务器本地:

# 假设你已经把arthas-packaging-xxx-bin.zip传到服务器 unzip arthas-packaging-xxx-bin.zip cd arthas-packaging-xxx-bin ./install-local.sh

安装完成后,安装目录下会有as.sh脚本。之后启动只需要:

./as.sh <pid>

它会优先使用本地安装的依赖,不会再联网下载。我第一次在客户内网环境部署时,就是因为没有提前准备完整包,导致boot引导半天卡在下载上。后来改成离线安装,一分钟内就接入了进程。

2.3 启动方式:交互式、非交互式、脚本化

Arthas默认启动后是一个交互式命令行,你敲一条命令,它回一条结果。适合人工排查。

但如果你想在告警触发时自动采集现场,或者想定时执行一些诊断命令,就需要用非交互模式:

java -jar arthas-boot.jar -c 'thread -n 5' <pid>

-c后面跟的是要执行的命令,执行完会自动退出。

如果你有一串命令要按顺序跑,可以写一个脚本文件,然后通过-f指定:

java -jar arthas-boot.jar -f /tmp/arthas-commands.txt <pid>

脚本文件里每行一条Arthas命令。这个模式我后面会再讲,先记住有这几种启动方式,因为要不要学Arthas,很大程度上取决于你能不能把它接到自己的运维自动化流程里。

2.4 版本选择:不要迷信“越新越好”,但更不要用三年前的旧包

Arthas的版本更新速度不慢,新版本会适配新JDK、修复bug、增加命令。JDK 8用老版本可能没问题,但如果你线上有JDK 11、JDK 17甚至更新的版本,一定要用较新的Arthas,否则attach阶段就会失败。

我见过一个项目,因为用了很旧的Arthas包,诊断JDK 17进程时报了一堆module not found之类的错误。后来换了新版包,一条命令就进去了。

好习惯是:每次接手新环境,先去查当前Arthas版本,再对照一下目标环境JDK版本。arthas-boot.jar -h可以查看帮助,部分版本支持--version查看版本号。如果发现旧版有个别命令报错,优先升级。

3. 第一条命令就该这么用:快速验证Arthas是否已经“贴”上你的Java进程

3.1 找到目标进程并attach

不要凭记忆猜PID,启动前先用jps -l看Java进程列表:

jps -l

假设得到输出:

12345 /data/app/order-service-1.0.jar 67890 sun.tools.jps.JPS

那目标PID就是12345。然后执行:

java -jar arthas-boot.jar 12345

如果attach成功,终端会打印Arthas的logo和一行提示:

[INFO] Try to attach process 12345 [INFO] Attach process 12345 success.

如果权限不够,它会报错,你再用sudo -u <运行用户> java -jar arthas-boot.jar 12345调整用户身份。

成功进入交互界面之后,我会先敲help,看看当前环境的命令列表是不是和预期一致。这既是验证Arthas是否完整,也是确认版本差异。

3.2 进入交互式命令行后的第一件事:看全局和线程

交互式命令行的第一屏,我会先用dashboard:

dashboard

它会刷出一个信息面板,显示当前JVM的CPU使用率、内存分布、GC次数、类加载数量,以及一部分线程状态。如果一上来就发现GC次数疯涨,说明可能是内存或GC配置问题;如果CPU高但GC正常,大概率是业务代码里的计算密集或死循环。

紧接着看繁忙线程:

thread -n 3

这个命令会按CPU占用从高到低列出Top 3线程,并给出它们当前的堆栈。这一步能直接告诉你,“是哪个线程在抢CPU”。

dashboard和thread是我在任何线上问题里必跑的两条命令,因为它们能快速缩小排查范围。

3.3 验证“操作感”:用sc和jad看真实类

为了确认Arthas确实“看”到了目标进程的类,可以搜一个已知的类:

sc com.example.order.service.OrderService

正常会输出类加载器、类的注解、字段、方法等信息。如果输出为空,说明类名对不上,或者该类还没有被加载。类都没有加载,自然也就谈不上诊断。

想确认线上真实代码和本地代码是否一致,就用反编译:

jad com.example.order.service.OrderService

你会看到当前JVM里实际运行的字节码反编译后的源码。注意,反编译出来的代码和原始源码并不完全一致:注释会丢失,匿名内部类会被拆成单独类,泛型信息可能被擦除。所以不要仅仅凭反编译结果断言“代码改过了”,要结合sc输出的类加载时间和版本信息一起判断。

3.4 退出Arthas,不等于杀掉业务进程

很多新手用Arthas时最担心的就是:我exit会不会把业务进程也关了?不会。exit或quit退出的是Arthas客户端会话,Arthas服务端也会在客户端断开后自动退出,但目标Java进程完全不受影响。

不过要留意另一个坑:Arthas在attach时会占用一个本机端口(默认3658),如果上一个会话异常退出,端口可能还被占用。下次启动时提示“端口被占用”,你需要先找到残留的Arthas进程并清理:

ps -ef | grep arthas

然后再启动新的会话。这个问题在多人同时诊断同一个进程时特别容易出现。

4. 实际排查案例:用一次CPU满核问题走完Arthas排错全链路

4.1 现象确认与准备

模拟一个很典型的线上事件:某订单服务的CPU占用持续在95%以上,接口P99从300ms涨到3s,但系统负载不算特别高,说明不是单纯的流量上涨,而是某个线程卡在死循环或者大量计算里。

登录服务器后,先jps -l找到目标进程,记录PID。然后用Arthas attach:

java -jar arthas-boot.jar 12345

进入交互界面后,第一屏跑dashboard,确认CPU确实高、GC没有明显异常。此时已经可以排除Full GC导致的问题,方向转向“某个线程占满了CPU”。

4.2 用thread精确锁定出问题的线程

执行:

thread -n 5

输出里会列出CPU占用最高的5个线程,包括线程ID、名称、状态、CPU时间、栈帧。正常情况下,你要找的是那种状态为RUNNABLE且CPU时间远高于其他线程的栈帧。

假设你看到下面的堆栈(示意):

"http-nio-8080-exec-12" Id=27 RUNNABLE at com.example.order.service.impl.OrderServiceImpl.lambda$createOrder$1(OrderServiceImpl.java:214) at com.example.order.service.impl.OrderServiceImpl.createOrder(OrderServiceImpl.java:180) ...

这说明不到一行,就已经锁定了问题所在:OrderServiceImpl的某个lambda逻辑在疯狂执行。这放在以前,你得先top -Hp找到线程号,转成十六进制,再jstack对线程号,眼睛对半天。现在一步到位。

如果thread -n 5看到最忙线程栈停在epollWait或park这种正常等待状态,反而说明CPU不是这些线程消耗的,可能是GC线程或者JIT编译线程,需要再看dashboard里的GC面板和thread的汇总信息。

4.3 顺着watch和trace追到根因

定位到具体方法之后,要判断它为什么变成了CPU热点。直接上watch,观测方法的入参和返回值:

watch com.example.order.service.impl.OrderServiceImpl createOrder "{params[0], returnObj}" -x 2

触发几次请求后,输出里会有入参和返回结果。如果发现某个参数一直在膨胀,比如list大小指数增长,说明有个无限循环在填充数据。如果入参正常,但方法体内有一个内部方法被反复调用,就要再往下追。

用trace看方法内部的真实调用耗时分布:

trace com.example.order.service.impl.OrderServiceImpl createOrder '#cost > 100'

#cost > 100意思是只打印耗时超过100ms的调用路径。它会列出每个子调用的耗时,包括调用次数。假设输出显示OrderServiceImpl里调用某工具类的hashCode()方法耗时占比99%,且调用了几十万次,那问题就水落石出了——可能是在循环里使用了一个会不断触发重新哈希的结构,比如不该在循环里调用的toString()或hashCode()。

有时候为了确认真实逻辑,再用jad看一下当前字节码反编译结果,确认和本地代码一致。这样既能定位根因,也能确认“是不是最近发布引入了这段坏代码”。

4.4 临时止血:redefine到底能不能用

定位到根因之后,如果紧急到没法等发布流程,Arthas留了一招redefine,可以用编译好的class替换JVM里已有的类。

前提是你已经有修复后的.class文件。比如你把修复后的OrderServiceImpl.class放到/tmp/目录下,然后执行:

redefine /tmp/OrderServiceImpl.class

执行成功后会提示redefine success。但我要提前泼盆冷水:

第一,redefine只能修改已有方法的方法体,不能新增方法、不能新增字段、不能改变类签名。如果你想做“加一个开关字段”这种修改,做不到。

第二,它不会自动持久化。类如果被重新加载,修改就丢了。

第三,如果修改后的代码有问题,你很难快速回滚到旧字节码。

所以我的建议是:redefine只用于紧急止血,而且必须配合完整的发布预案。大多数情况下,Arthas负责把根因找出来,真正的修复还是要走正常发布流程。不要图省事把热更新当日常习惯。

4.5 复盘:一次排查的命令清单和时间线

把上面的排查动作整理成清单,方便以后照抄:

  1. jps -l找到目标进程PID。
  2. java -jar arthas-boot.jar <pid>进入交互模式。
  3. dashboard排除GC异常。
  4. thread -n 5锁定CPU热点线程和栈帧。
  5. jad 类名确认线上代码。
  6. watch 类名 方法名 "{params[0], returnObj}" -x 2观察关键参数。
  7. trace 类名 方法名 '#cost>100'找到耗时子调用。
  8. 分析根因,紧急止血或走发布流程。

这套流程我实际用过很多次,从attach到定位根因,基本控制在5分钟内。比加日志重发快一个量级。

5. 生产环境使用Arthas的安全红线与权限控制

5.1 attach本身会不会影响线上进程

先给结论:正常使用诊断命令,对目标进程影响很小,但不是零。

启动Arthas时会有一次attach动作,这个动作需要目标JVM配合,可能产生一次短暂的停顿,但通常在毫秒级。真正需要警惕的是那些高频观测命令,比如对某个QPS极高的方法watch不加条件,或者trace打印频繁调用的路径。字节码增强和每次调用时的额外计算,都会增加少量CPU和内存开销。

所以生产环境使用有一条红线:不要在业务高峰期长时间挂大范围观测命令。你可以用条件过滤,比如#cost > 1000只看慢调用;也可以等低峰期排查。用完就exit,千万别把Arthas当常驻agent开着。

还有一点:redefine是改字节码的操作,属于高敏感动作,必须有变更审批和回滚方案,否则一个小失误会直接影响业务。

5.2 权限、审计和远程访问控制

Arthas默认支持通过telnet端口(3658)连接,也就是说,如果服务器防火墙没拦住,别人可以telnet到你的Arthas会话端口来执行命令。生产环境必须默认关闭远程连接,或者修改监听地址为本地回环,只允许本机操作。

操作审计上,最稳妥的办法是所有Arthas操作在跳板机上执行,并记录操作日志。你可以用Linux的script命令录制终端会话,或者把Arthas输出重定向到文件,作为变更依据。不要小看这件事,线上操作如果无痕,出了问题很难追溯。

权限控制方面,不要给所有人都能attach生产进程的权限。至少要做到:生产服务器只有指定的几位负责人有操作权限,其他人只能申请。如果团队里有新人,建议先去预发环境练熟命令再上生产。

5.3 敏感信息:watch和ognl会“偷看”数据

watch输出方法入参和返回值,ognl可以读取对象字段。这些命令如果打在用户维度的服务上,很容易把手机号、身份证、密码摘要、token之类的东西打印到屏幕上。

我见过有人排查一个下单问题,watch把整个订单对象的Json形式打印出来了,里面全是用户地址和联系电话,然后顺手截图发到了工作群。这个习惯非常危险。

我的建议是,生产环境执行watch时先想清楚:这个方法会不会带敏感字段?如果只需要看长度,就用params[0].size(),不要输出整个对象;如果需要脱敏,使用ognl表达式只取非敏感字段。

5.4 容器环境和大规模集群的特有问题

容器环境下,Arthas的使用比物理机多一些注意点。

Docker容器默认没有足够权限去attach另一个进程。启动容器时需要添加SYS_PTRACE能力,或者用--privileged。如果镜像里只有JRE,没有完整JDK,有些attach函数会找不到,建议生产镜像尽量使用完整JDK的基础镜像。

Kubernetes场景下,你是进入pod执行命令的,所以受pod自身的securityContext限制。如果pod没开特权模式,Arthas很可能诊断不了同一个pod里的Java进程。解决方式是在部署时提前为带Java服务的pod配置好对应的安全上下文,而不是出问题时临时抱佛脚。

另外,在大规模集群里,不要一台台手工attach。Arthas虽然提供了tunnel等方式做集中管理,但配置复杂度不低。通常我的做法是:先用监控系统发现可疑节点,再在单台机器上attach进去做深度诊断,而不是遍地撒网。Arthas是“手术刀”,不是“雷达”。

6. 安装使用中我踩过的坑与最终建议

6.1 老版本JDK与新JDK的兼容性问题

如果你还在用JDK 6、JDK 7,很多新版Arthas可能不太兼容。反过来,如果你用太老的Arthas去诊断JDK 11+,也会碰到module相关的报错。

一个比较稳的策略是:线上所有Java服务尽量统一JDK大版本,Arthas随版本升级保持更新。我在某项目里见过JDK 8和JDK 17混部,每次排查都要确认当前进程到底是什么JDK,非常麻烦。后来推进统一JDK基线,再配合相同版本的Arthas包,问题少了很多。

如果诊断JDK 17时出现java.lang.reflect.InaccessibleObjectException,通常要给JVM加--add-opens参数。具体加哪些,可以根据报错信息逐条加。Arthas新版一般会在启动脚本里自动处理一部分,遇到的话先升级Arthas再说。

6.2 attach失败、端口占用的排查链路

attach失败最常见的三种表现:

第一种是“找不到进程”。在容器里执行java -jar arthas-boot.jar时,它默认找的是容器内的Java进程,如果PID不对,就会提示找不到。先jps -l看进程列表,确保PID正确。

第二种是权限问题。报错里通常会有Unable to open socket file之类的话。原因是你没有目标进程的系统权限。解决办法是用目标进程的用户身份执行Arthas,例如sudo -u appuser java -jar arthas-boot.jar <pid>。

第三种是端口占用。上一次Arthas没正常退出,3658被占用,会报port already in use。先用lsof -i:3658或netstat -ano | grep 3658找残留进程,杀掉后重试。还没解决就检查防火墙和hosts配置。

6.3 把Arthas接入自动化采集

如果每次都要人工登服务器打命令,Arthas的价值只发挥了一半。它支持非交互式执行,这给了我们接入自动化告警联动的好机会。

举个例子,监控系统发现某台机器CPU超过阈值时,自动在目标机器上执行:

java -jar arthas-boot.jar -c 'thread -n 10' <pid> > /data/logs/arthas-$(date +%s).log 2>&1

这样在告警发生时,就能自动留下一份线程快照,不用等人工登录时现场已经“凉了”。还可以组合多条命令,用-f指定脚本文件,一次性抓取多个维度的现场。

但注意别写每5分钟跑一次这种“钓鱼执法”式的定时任务。attach本身就带花销,频繁attach会影响线上进程稳定性。我一般只在告警触发和手工诊断时用。

6.4 最后几条实操建议

第一,不要只背命令,要理解每个命令背后的概念。比如thread输出的线程状态是什么含义,dashboard里的GC曲线怎么读,类加载器的层级是怎么回事。概念准了,命令才有用。

第二,线上使用前,先在本地或预发环境按“发现问题、定位、恢复”的流程完整演练一遍。我刚开始用Arthas时,就是先拿自己开发环境的例子练手,把watch、trace、jad、thread这些命令的格式都跑熟了,才敢在生产上放开。

第三,尽量结合监控体系使用。Arthas负责“深挖”,APM和Metrics负责“广撒网”。你不可能每台机器都开着Arthas,但你也别等问题出现时才发现没有任何监控。

用到现在,我的体会是:Arthas最值钱的部分不是那几条命令,而是它改变了线上问题的处理方式。以前我们遇到诡异故障,习惯性靠重启和玄学;现在有了一把能被精准使用的“手术刀”,能让你在几分钟内看到方法级、调用级、甚至字段级的真实运行细节。当然,工具再强也只是手段,真正能让线上稳定的,还是你对自己的代码、对JVM运行机制的理解足够深。所以,看完这篇文章,去环境里亲手跑一遍命令,再结合你手头真实的服务日志,把这条链路走通一次,比什么都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询