JVM 之 线上诊断神器Arthas实战【内部原理?常用命令?如何使用Arthas排查cpu飙高、类加载问题、死锁、慢接口等线上问题?】

Arthas 是什么?

简单讲,他是一款开源的线上诊断工具
可以在,不重启应用的前提下,对服务进行实时监控,诊断,调试,甚至进行热修复

  • 官方文档:https://arthas.aliyun.com/doc/

Arthas 解决了什么问题?

  • 1、线上debug只能加日志-》打包-》重新部署-》等待复现。
  • 2、性能瓶颈(慢方法、cpu飙高、内存泄漏)难以定位
  • 3、类加载异常难定位(ClassNotFoundException、NoSuchMethodError等,不知道从哪加载的?是否被覆盖)
  • 4、逻辑调用链不清晰(方法被谁调用的?参数是什么?)
  • 5、紧急bug只能停机重新发布

适用场景

  • 适合:线上紧急问题、性能问题、类加载问题排查,以及临时调试
  • 不适合:因为Arthas需手动交互+数据不存储,所以不适合自动化或长期监控。

Arthas 是怎么解决的这些问题

说白了,Arthas利用了Java Agent+Bytecode Instrumentation(字节码增强) 技术,
在运行时动态注入探针,实现对 JVM 内部状态的非侵入式观测与干预

Java Agent: 允许在不修改源代码、不重启应用的前提下,对程序进行监控、诊断、性能分析、日志增强、安全审计甚至热修复


Arthas提供了哪些能力?

能力说明
实时监控查看方法入参、返回值、异常(watch
性能剖析分析方法耗时、生成火焰图(trace / profiler
类信息查询查看类从哪个 JAR 加载、反编译字节码(sc / jad
热更新动态替换类定义,修复线上 Bug(redefine
JVM 全局视图实时查看线程、内存、GC、系统负载(dashboard
调用链追踪查看方法被谁调用、调用栈(stack

所有操作 无需重启应用无需改代码秒级生效


Arthas如何使用?

1、安装(任选其一)

# 方式一:快速启动(推荐)
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

# 方式二:脚本安装(Linux/macOS)
curl -L https://arthas.aliyun.com/install.sh | sh
./as.sh

2、启动

  1. 运行 java -jar arthas-boot.jar-》列出当前机器上所有 Java 进程
  2. 输入目标进程的编号,进入 Arthas 命令行(提示符如 [arthas@12364]$

在这里插入图片描述

启动时如指定 HTTP 端口java -jar arthas-boot.jar --http-port 8563,可在浏览器访问:http://<服务器IP>:8563图形化界面。


3、Arthas提供了哪些命令?

# 查看系统实时状态(线程、内存、GC)
dashboard

# 反编译某个类(确认线上代码是否正确)
jad com.example.service.UserService

# 监控方法入参和返回值(“这个方法执行时参数/返回值/异常是什么?” → 监控 方法内部数据)
watch com.example.service.UserService login '{params, returnObj}' -x 2

# 追踪方法调用耗时(定位慢接口)
trace com.example.controller.UserController getUser

# 查看某个方法的调用栈(“谁调用了这个方法?” → 查看 调用栈(Call Stack))
stack com.example.service.UserService saveUser

# 动态修改日志级别(无需重启)
logger --name com.example.service.UserService --level debug

# 搜索已加载的类
sc *UserService*

# 搜索类的方法
sm com.example.service.UserService *

# 生成火焰图(需 profiler 支持)
profiler start
# ...等待几秒...
profiler stop

如何使用Arthas进行线上具体场景问题排查?

发现现象 → 确定排查思路 → 使用Arthas排查 → 解决问题

常见场景速查表

问题类型关键命令核心输出
CPU 100%thread -n 3jadprofiler高 CPU 线程 + 热点代码
死锁thread -b死锁线程对 + 锁依赖关系
慢接口tracewatchstack耗时分布 + 参数/异常
类加载问题scjadclassloader类加载情况 + 类反编译内容

场景一:CPU飙高

现象

  • 1、应用响应变慢甚至无响应
  • 2、服务器负载飙升,top 显示某个 Java 进程 CPU 占用接近 100%

排查思路

CPU飙高,通常是因为无限循环、正则回溯、复杂计算等引起的。
所以排查思路是:

  • 1、找出哪个线程在疯狂占用 CPU?
  • 2、这个线程在执行什么代码?

Arthas排查定位步骤

  • 1、启动 Arthas 并 attach 到目标进程java -jar arthas-boot.jar 并进入目标服务进程
  • 2、查看 CPU使用率最高 的前 3 个线程 thread -n 3
    关键信息:线程名 Thread-10,ID=45,CPU占用 98.7%,卡在 DataProcessor.java:28
    执行后,输出示例如下:
    Threads Total: 50, NEW: 0, RUNNABLE: 10, BLOCKED: 0, WAITING: 20, TIMED_WAITING: 20
    "Thread-10" Id=45 cpuUsage=98.7% RUNNABLE
        at com.example.service.DataProcessor.process(DataProcessor.java:28)
        at com.example.service.DataProcessor.lambda$start$0(DataProcessor.java:15)
        ...
    
  • 3、反编译该类确认代码逻辑(是否有死循环或低效算法)jad com.example.service.DataProcessor
    例如:
    while (true) { // ← 问题在这里!
        list.add(new Object());
    }
    
  • 4、生成火焰图,并下载到本地,用浏览器打开,观察其中的热点方法。
    profiler start
    # 等待 10 秒,然后输出结果文件 arthas-output/xxx.html
    profiler stop --format html
    
  • 5、定位到具体原因,修改后重新部署

场景二:死锁(Deadlock)

现象

  • 1、请求全部超时(应用完全卡住)
  • 2、日志中无异常信息,但日志不再输出(线程不再处理新任务)

排查思路

死锁通常由 多个线程互相持有对方需要的锁 导致。JVM 能自动检测死锁。

Arthas排查定位步骤

  • 1、启动 Arthas 并 attach 到目标进程java -jar arthas-boot.jar 并进入目标服务进程
  • 2、检查是否存在死锁 thread -b

    -b 参数表示 detect deadlock(检测死锁)

    • 存在死锁则会输出死锁的具体信息:(以下Thread-A、Thread-B相互引用,典型死锁)
        Found one Java-level deadlock:
        =============================
        "Thread-A":
          waiting to lock monitor 0x00007f8b4c003a88 (object 0x000000076b8d1234, a java.lang.Object),
          which is held by "Thread-B"
        "Thread-B":
          waiting to lock monitor 0x00007f8b4c004b99 (object 0x000000076b8d1240, a java.lang.Object),
          which is held by "Thread-A"
      
        Java stack information for the threads listed above:
        ===================================================
        "Thread-A":
            at com.example.service.OrderService.pay(OrderService.java:30)
            - waiting to lock <0x000000076b8d1234> (a java.lang.Object)
            at com.example.controller.OrderController.submit(OrderController.java:22)
        ...
    
  • 3、查看完整线程栈
    thread  # 查看所有线程状态
    # 或指定线程 ID
    thread 45
    
  • 4、根据线程栈找出具体死锁位置及原因,并加以解决重新部署。

场景三:慢接口

现象

其他接口正常,只有某个接口响应时间暴涨(例如:100ms-》5s+)

排查思路

可能的原因:

  • 1、是不是数据库慢查询
  • 2、是不是 调用的其他服务的http接口超时 ,进而导致的超时
  • 3、本地方法逻辑复杂(如大循环、序列化)

此时需要 追踪整个调用链的耗时分布

Arthas排查定位步骤

假设慢接口对应 Controller 方法:com.example.controller.UserController.getUser

  • 1、使用 trace 命令追踪方法调用耗时trace com.example.controller.UserController getUser
    • 1.1、然后触发一次请求,使Arthas输出调用链路(如 curl http://localhost:8080/user/123
      `---ts=2025-12-13 17:40:01;thread_name=http-nio-8080-exec-2;id=2a;is_daemon=true;priority=5;TCCL=org.springframework.boot...
          `---[5023.45ms] com.example.controller.UserController:getUser()
              +---[0.12ms] com.example.service.UserService::findById()
              +---[5022.89ms] com.example.service.NotificationService::sendEmail() # ← 耗时 5 秒!
              `---[0.05ms] return result
      
    • 1.2、分析调用链路,找出耗时原因。(例如上诉:sendEmail() 花了 5 秒,可能是 SMTP 超时或网络问题。)
    • 1.3、重复上诉步骤,找到最终的那个慢方法。(例如:继续分析 sendEmail 方法内部,trace com.example.service.NotificationService sendEmail
  • 2、监控慢方法的参数和返回值,确定问题出现的原因。
    watch com.example.service.NotificationService sendEmail '{params, returnObj, throwExp}' -v -x 2
    ## 可看到是否传入了错误邮箱、是否抛出异常等。
    
  • 3、可通过热更新,先保证线上服务可用(临时解决,应急,例如跳过该逻辑)
    • 3.1. 修改 UserController.java,注释掉 notificationService.sendEmail(...)
    • 3.2. 编译生成 .class
    • 3.3. 执行:redefine /tmp/UserController.class
  • 4、根本上解决该问题,并发布更新。

场景四:类加载异常

现象

异常常见原因
ClassNotFoundException类根本不存在于 classpath
NoClassDefFoundError编译时存在,运行时缺失(如依赖未打包)
NoSuchMethodError方法签名不一致(通常是版本冲突:A 依赖 v1,B 依赖 v2)
IncompatibleClassChangeError类结构不兼容(如接口变抽象类)
LinkageError / ClassCastException同一个类被不同 ClassLoader 加载

排查思路

核心问题本质

  • 类找不到? → 检查是否在 classpath
  • 类找到了但不对? → 检查是否被错误版本覆盖
  • 同一个类加载了多份? → 检查 ClassLoader 隔离问题

Arthas 排查思路(四步法):

  • 1、类是否被加载? → 使用 sc(Search Class)
  • 2、如果已加载,从哪加载的? → 使用 sc -d 查看类的详细信息(含 ClassLoader 和 codeSource)
  • 3、反编译字节码,确认方法/字段是否存在? → 使用 jad 查看实际加载的代码
  • 4、检查类加载器层次,分析是否存在冲突? → 使用 classloader 命令分析 ClassLoader 树

Arthas排查定位详细步骤

假设有以下报错java.lang.NoSuchMethodError: com.example.util.StringUtils.isBlank(Ljava/lang/String;)Z
根据报错怀疑 StringUtils 被低版本 JAR 覆盖

  • 1、搜索该类是否被加载sc *StringUtils*
    • 发现存在两个 StringUtils!需进一步确认用的是哪个:
    com.example.util.StringUtils
    org.apache.commons.lang3.StringUtils
    
  • 2、查看具体类的加载信息(关键!)sc -d com.example.util.StringUtils
    • 输出示例:(重点看 code-sourceclass-loader
       class-info        com.example.util.StringUtils
       code-source       /app/lib/utils-1.0.jar   ← ⚠️ 关键:来自哪个 JAR!判断是不是你所期望的那个?(`code-source` 为空,可能是从 `classes` 目录加载(非 JAR))
       name              com.example.util.StringUtils
       isInterface       false
       isAnnotation      false
       isEnum            false
       isAnonymousClass  false
       isArray           false
       isLocalClass      false
       isMemberClass     false
       isPrimitive       false
       isSynthetic       false
       simple-name       StringUtils
       modifier          public
       annotation
       interfaces
       super-class       java.lang.Object
       class-loader      org.springframework.boot.loader.LaunchedURLClassLoader@1c20c6b4
       class-loader-hash 1c20c6b4
      
  • 3、反编译确认方法是否存在:jad com.example.util.StringUtils
    • 例如以下输出片段:(没有 isBlank 方法,所以抛出 NoSuchMethodError
    public class StringUtils {
        public static boolean isEmpty(String str) {
            return str == null || str.length() == 0;
        }
        // 注意:没有 isBlank 方法!
    }
    
  • 4、检查是否有多个版本的 JAR?
    • 方法 A:列出所有 JAR 中的该类(需知道可能路径)
      # 查看 classpath 下所有 JAR
      classloader -l
      
    • 方法 B:搜索所有 ClassLoader 中的该类(Arthas 3.5+)
      # `-a` 表示 all classloaders,可发现不同 ClassLoader 加载了不同版本。
      sc -a *StringUtils*
      
    • 方法 C:查看某个 ClassLoader 加载了哪些资源
      # 先获取 ClassLoader hash(从 sc -d 输出中拿到,如 1c20c6b4)
      classloader -c 1c20c6b4 -r com/example/util/StringUtils.class
      # 输出:file:/app/lib/utils-1.0.jar!/com/example/util/StringUtils.class
      
  • 5、对比期望版本 vs 实际版本
    • 5.1. 将正确版本的 StringUtils.class 上传到服务器
    • 5.2. 用 Arthas 反编译对比:
      # 反编译线上加载的
      jad --source-only com.example.util.StringUtils > online.java
      
      # 反编译你本地正确的(先放到 /tmp/correct/ 目录)
      jad --source-only -c /tmp/correct/StringUtils.class > correct.java
      
      # diff 对比
      diff online.java correct.java
      

总结:类加载问题排查流程图

Arthas 的 sc + jad + classloader 组合拳,是解决此类问题的“黄金三角”
在这里插入图片描述


使用实用建议

  • 1、精准监控特定参数
# 只监控用户名为 "admin" 的登录请求
watch com.example.service.UserService login 'params[0]=="admin"' -v
  • 2、限制监控次数
# 只捕获前 3 次调用
watch com.example.service.UserService login '{params, returnObj}' -n 3
  • 3、结合 OGNL 表达式过滤
watch com.example.service.OrderService createOrder 'params[0].amount > 1000' -x 3
  • 4、快速定位 CPU 飙高
# 查看最耗 CPU 的线程
thread -n 3

# 结合 jstack 分析死锁
thread -b
  • 5、热修复(谨慎使用!)
# 1. 修改本地 .java 文件并编译成 .class
# 2. 上传到服务器
# 3. 执行
redefine /tmp/UserService.class

注意:redefine 不能增减字段/方法,仅支持方法体修改。

  • 6、导出堆快照(用于 MAT 或 JProfiler 分析内存泄漏。)
heapdump --live /tmp/heap.hprof
  • 7、类冲突定位:
    • 7.1、模糊搜索 + 包过滤(只搜自己项目的类,避免第三方干扰)sc com.yourcompany.*Service
    • 7.2、结合异常堆栈定位具体类(从异常日志中提取完整类名,直接 sc -d 它)
    • 7.3、注意内部类写法
      # 内部类要用 $ 分隔
      sc com.example.OuterClass$InnerClass
      jad com.example.OuterClass$InnerClass
      
    • 7.4、Spring Boot 特别注意
      • Spring Boot 使用 LaunchedURLClassLoader
      • 类通常来自 BOOT-INF/classesBOOT-INF/lib/xxx.jar
      • 如果 sc -d 显示 code-source 为空,可能是从 classes 目录加载(非 JAR)

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值