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

Arthas 是什么?

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


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、安装(任选其一)

bash 复制代码
# 方式一:快速启动(推荐)
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提供了哪些命令?

bash 复制代码
# 查看系统实时状态(线程、内存、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 <math xmlns="http://www.w3.org/1998/Math/MathML"> s t a r t start </math>start0(DataProcessor.java:15) ...

  • 3、反编译该类确认代码逻辑(是否有死循环或低效算法)jad com.example.service.DataProcessor

    java 复制代码
    例如:
    while (true) { // ← 问题在这里!
        list.add(new Object());
    }
  • 4、生成火焰图,并下载到本地,用浏览器打开,观察其中的热点方法。

    bash 复制代码
    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相互引用,典型死锁)
    vbnet 复制代码
        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、查看完整线程栈

    bash 复制代码
    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、监控慢方法的参数和返回值,确定问题出现的原因。

    bash 复制代码
    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
    java 复制代码
    public class StringUtils {
        public static boolean isEmpty(String str) {
            return str == null || str.length() == 0;
        }
        // 注意:没有 isBlank 方法!
    }
  • 4、检查是否有多个版本的 JAR?

    • 方法 A:列出所有 JAR 中的该类(需知道可能路径)

      bash 复制代码
      # 查看 classpath 下所有 JAR
      classloader -l
    • 方法 B:搜索所有 ClassLoader 中的该类(Arthas 3.5+)

      bash 复制代码
      # `-a` 表示 all classloaders,可发现不同 ClassLoader 加载了不同版本。
      sc -a *StringUtils*
    • 方法 C:查看某个 ClassLoader 加载了哪些资源

      bash 复制代码
      # 先获取 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 反编译对比:

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

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

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

  • 6、导出堆快照(用于 MAT 或 JProfiler 分析内存泄漏。)
bash 复制代码
heapdump --live /tmp/heap.hprof
  • 7、类冲突定位:
    • 7.1、模糊搜索 + 包过滤(只搜自己项目的类,避免第三方干扰)sc com.yourcompany.*Service

    • 7.2、结合异常堆栈定位具体类(从异常日志中提取完整类名,直接 sc -d 它)

    • 7.3、注意内部类写法

      bash 复制代码
      # 内部类要用 $ 分隔
      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)

相关推荐
透明的玻璃杯5 小时前
sqlite数据库连接池
jvm·数据库·sqlite
7ioik5 小时前
jvm性能检测及调优?
jvm
何中应5 小时前
【面试题-4】JVM
java·jvm·后端·面试题
7ioik5 小时前
jvm垃圾回收算法?
jvm·算法
没有bug.的程序员20 小时前
高频IO服务优化实战指南
java·jvm·spring·容器
Donald_brian1 天前
线程同步
java·开发语言·jvm
喵了meme1 天前
Linux学习日记19:线程同步与互斥锁
java·jvm·学习
小小Fred1 天前
Cortex-M3 LR寄存器的特殊值EXC_RETURN
java·开发语言·jvm
YANshangqian1 天前
家具设计软件Room Arranger Portable
jvm