
Arthas是阿里巴巴2018年开源的Java诊断工具,基于Java Instrumentation API和ASM字节码框架开发,支持JDK 6及以上版本,可运行在Linux、Mac、Windows等多种环境,兼容Spring Boot、Dubbo、Tomcat等主流Java框架。其核心定位是“运行时诊断”,无需修改代码、无需重启应用,即可实现对Java应用的动态监控、问题排查与热更新。
通过官方脚本快速下载最新稳定版(当前最新稳定版:3.7.6):
# Linux/Mac环境
curl -O https://arthas.aliyun.com/arthas-boot.jar
# Windows环境直接访问链接下载:https://arthas.aliyun.com/arthas-boot.jar
# 启动arthas-boot.jar,自动识别当前运行的Java进程
java -jar arthas-boot.jar
# 输入进程对应的序号(如1、2),即可附着到目标JVM进程
启动成功后,将进入Arthas交互控制台,显示如下信息:
[INFO] arthas-boot version: 3.7.6
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 12345 com.jam.demo.ArthasDemoApplication
[2]: 67890 org.apache.catalina.startup.Bootstrap
1
[INFO] Attach process 12345 success.
[INFO] arthas home: /root/.arthas/lib/3.7.6/arthas
[INFO] The target process already loaded arthas-agent, skip install.
[INFO] Arthas server already started on port 3658, let's connect it...
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
进入控制台后,可通过以下命令验证环境是否正常:
# 查看Arthas版本
version
# 查看当前附着的进程信息
process
# 查看帮助文档
help
若需远程连接目标JVM进程,需在启动时指定端口:
# 目标进程所在机器启动Arthas,指定IP和端口(0.0.0.0表示允许所有IP访问)
java -jar arthas-boot.jar --host 0.0.0.0 --port 3658 12345
# 本地机器通过telnet/ssh连接
telnet 目标机器IP 3658
# 或通过arthas-client连接
java -jar arthas-client.jar 目标机器IP 3658
要理解Arthas的工作原理,必须先掌握两个核心技术:Java Instrumentation API和JVM Attach机制。这两个技术是Arthas实现“无侵入式诊断”的基础。
Java Instrumentation(简称Instrument)是JDK 5引入的一套API,允许外部程序在JVM运行时修改类的字节码,实现对类的增强。其核心能力包括:
Instrument的核心是java.lang.instrument.Instrumentation接口,关键方法如下:
/**
* 注册字节码转换器,后续所有类加载都会经过该转换器
* @param transformer 自定义的字节码转换器
* @param canRetransform 是否支持重新转换(true表示可重复修改)
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
/**
* 重定义已加载的类,替换类的字节码
* @param definitions 类定义数组,包含类对象和新的字节码
* @throws ClassNotFoundException 类未找到
* @throws UnmodifiableClassException 类无法修改(如final类)
*/
void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
/**
* 获取JVM中已加载的所有类
* @return 已加载类的数组
*/
Class<?>[] getAllLoadedClasses();

JVM Attach机制是JDK提供的一种进程间通信能力,允许一个Java进程(如Arthas客户端)附着到另一个运行中的Java进程(目标应用),并加载代理程序(agent)。其核心类是com.sun.tools.attach.VirtualMachine。

tools.jar(JDK 9+已模块化,需引入模块依赖);Arthas的工作原理本质是“Attach机制注入代理 + Instrumentation实现字节码增强”的组合,整体流程如下:

Arthas采用“客户端-服务端”架构,分为三大核心模块:客户端、服务端、代理端,各模块职责清晰,协同工作实现诊断功能。

Arthas客户端与服务端通过TCP协议通信,默认端口3658,采用自定义的消息格式:
字节码增强是Arthas所有核心功能(如监控、追踪、热更新)的基础。Arthas底层采用ASM框架操作字节码,相比Javassist等框架,ASM更高效、轻量,直接操作字节数组,无反射开销。
Java源码编译后生成Class文件,Class文件是一组8字节的二进制流,包含类的基本信息(类名、父类、接口)、字段信息、方法信息、常量池等。字节码增强本质是修改Class文件中的方法体指令,实现插桩逻辑。
ASM框架提供了一套API,可直接读取、修改、生成Class文件的字节码,核心类包括:
ClassReader:读取Class文件的字节码,解析为抽象语法树;ClassWriter:根据抽象语法树生成修改后的字节码;ClassVisitor:遍历抽象语法树,在指定位置插入自定义指令(如方法入口、出口)。Arthas支持两种核心插桩方式:
以下是一个简化的ASM插桩示例,实现方法耗时统计(Arthas监控命令的底层核心逻辑):
package com.jam.demo.arthas.asm;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* 字节码增强工具类,为方法添加耗时统计
* @author ken
*/
publicclass TimeCostTransformer {
/**
* 增强指定类的指定方法,添加耗时统计
* @param className 类名(全限定名)
* @param methodName 方法名
* @param originalBytes 原始类字节码
* @return 增强后的类字节码
*/
publicstaticbyte[] enhance(String className, String methodName, byte[] originalBytes) {
// 1. 读取原始字节码
ClassReader classReader = new ClassReader(originalBytes);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// 2. 创建自定义ClassVisitor,遍历类结构
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9, classWriter) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
// 只增强目标方法
if (name.equals(methodName)) {
returnnew MethodVisitor(Opcodes.ASM9, mv) {
@Override
public void visitCode() {
// 前置插桩:记录开始时间
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("方法开始执行");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(Opcodes.LSTORE, 1);
super.visitCode();
}
@Override
public void visitInsn(int opcode) {
// 后置插桩:计算耗时(正常返回时)
if (opcode == Opcodes.RETURN || opcode == Opcodes.ARETURN || opcode == Opcodes.IRETURN) {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(Opcodes.LLOAD, 1);
mv.visitInsn(Opcodes.LSUB);
mv.visitVarInsn(Opcodes.LSTORE, 3);
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("方法执行耗时:");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);
mv.visitVarInsn(Opcodes.LLOAD, 3);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
}
super.visitInsn(opcode);
}
};
}
return mv;
}
};
// 3. 遍历并修改字节码
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
// 4. 返回修改后的字节码
return classWriter.toByteArray();
}
}
UnsupportedOperationException;final类、final方法(JVM限制);Arthas的核心能力依赖动态字节码增强技术,其底层基于Java Instrumentation API(JDK 5+引入)和ASM字节码操作框架,通过“Attach机制注入代理 -> 注册ClassFileTransformer -> 重定义目标类”的流程实现无侵入式监控。以下是详细实现流程和原理拆解:

com.sun.tools.attach.VirtualMachine,通过attach(pid)方法建立连接,再通过loadAgent(agentJarPath)注入代理。addTransformer(ClassFileTransformer)注册字节码转换器,redefineClasses(ClassDefinition...)触发类重定义,支持在类加载时或运行时修改字节码。transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)方法,对目标类的字节码进行修改后返回。以下通过“修改用户服务的getUserById方法,添加耗时统计和日志输出”为例,演示Arthas字节码增强的实际效果:
package com.jam.demo.arthas.service;
import com.jam.demo.arthas.entity.User;
import com.jam.demo.arthas.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
/**
* 用户服务类
* @author ken
*/
@Slf4j
@Service
publicclass UserService {
@Resource
private UserMapper userMapper;
/**
* 根据用户ID查询用户信息
* @param userId 用户ID(非空)
* @return 用户实体
* @throws IllegalArgumentException 当userId为空时抛出
*/
public User getUserById(Long userId) {
// 入参校验(符合阿里开发手册:入参非空校验)
if (ObjectUtils.isEmpty(userId)) {
thrownew IllegalArgumentException("用户ID不能为空");
}
log.info("开始查询用户信息,用户ID:{}", userId);
// 模拟业务耗时(实际场景为数据库查询)
try {
Thread.sleep(200);
} catch (InterruptedException e) {
log.error("查询用户信息时线程中断", e);
Thread.currentThread().interrupt();
}
return userMapper.selectById(userId);
}
}
getUserById(1L),日志输出如下:2024-05-20 14:30:00.123 INFO 12345 --- [nio-8080-exec-1] c.j.d.arthas.service.UserService : 开始查询用户信息,用户ID:1
# 启动Arthas(最新稳定版3.7.2)
java -jar arthas-boot.jar
# 选择目标进程(输入对应序号)
UserService类,确认方法结构:jad com.jam.demo.arthas.service.UserService getUserById
UserServiceEnhanced.java:package com.jam.demo.arthas.service;
import com.jam.demo.arthas.entity.User;
import com.jam.demo.arthas.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import javax.annotation.Resource;
/**
* 用户服务类(Arthas增强版)
* @author ken
*/
@Slf4j
@Service
publicclass UserService {
@Resource
private UserMapper userMapper;
public User getUserById(Long userId) {
// 增强逻辑:添加耗时统计
long start = System.currentTimeMillis();
try {
if (ObjectUtils.isEmpty(userId)) {
thrownew IllegalArgumentException("用户ID不能为空");
}
log.info("开始查询用户信息,用户ID:{}", userId);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
log.error("查询用户信息时线程中断", e);
Thread.currentThread().interrupt();
}
return userMapper.selectById(userId);
} finally {
long cost = System.currentTimeMillis() - start;
log.info("查询用户信息结束,用户ID:{},耗时:{}ms", userId, cost);
}
}
}
# 编译命令(需JDK17环境,指定依赖包路径)
javac -cp "arthas-boot.jar:spring-boot-starter.jar:lombok-1.18.30.jar:mybatis-plus-boot-starter.jar:${CLASSPATH}" com/jam/demo/arthas/service/UserServiceEnhanced.java
redefine命令加载增强后的字节码:# 重定义类(替换原始UserService)
redefine com/jam/demo/arthas/service/UserServiceEnhanced.class
getUserById(1L),日志输出如下(新增耗时统计):2024-05-20 14:35:00.456 INFO 12345 --- [nio-8080-exec-2] c.j.d.arthas.service.UserService : 开始查询用户信息,用户ID:1
2024-05-20 14:35:00.658 INFO 12345 --- [nio-8080-exec-2] c.j.d.arthas.service.UserService : 查询用户信息结束,用户ID:1,耗时:202ms
UnsupportedOperationException。redefine命令不支持修改final方法、static方法(部分JVM版本),且修改后的类不会持久化到磁盘,应用重启后失效。Arthas提供了数十个命令,覆盖类查询、方法监控、性能分析、JVM诊断等场景。以下是生产环境最常用的核心命令,结合实例详细讲解其用法、底层原理和最佳实践:
Class.getResourceAsStream(".class")获取类的字节码,再通过ASM框架反编译为Java代码。jad [选项] 类名 [方法名]-c <classLoaderHash>:指定类加载器(多个类加载器加载同一类时使用)-E:开启正则表达式匹配方法名# 反编译UserService类的所有方法
jad com.jam.demo.arthas.service.UserService
# 仅反编译getUserById方法
jad com.jam.demo.arthas.service.UserService getUserById
# 当存在多个类加载器时,指定类加载器(通过sc命令获取hash值)
jad -c 1b6d3586 com.jam.demo.arthas.service.UserService
Instrumentation.getAllLoadedClasses()获取所有已加载类,再根据条件过滤。sc [选项] 类名表达式-d:显示详细信息(类加载器、父类、接口、实例数等)-f:显示类的字段信息-m:显示类的方法信息-c <classLoaderHash>:指定类加载器# 搜索所有包含User的类(模糊匹配)
sc *User*
# 查看UserService的详细信息(包括类加载器)
sc -d com.jam.demo.arthas.service.UserService
# 查看UserService的字段和方法信息
sc -f -m com.jam.demo.arthas.service.UserService
Class.getDeclaredMethods()获取类的所有方法,解析方法的访问修饰符、参数类型、返回值类型。sm [选项] 类名 [方法名表达式]-d:显示详细信息(方法签名、访问修饰符、异常类型等)-E:开启正则表达式匹配方法名# 查看UserService的所有方法
sm com.jam.demo.arthas.service.UserService
# 查看getUserById方法的详细签名
sm -d com.jam.demo.arthas.service.UserService getUserById
monitor [选项] 类名 方法名-c <周期>:指定监控周期(单位:秒,默认10秒)-b:监控方法调用前的参数-e:监控方法抛出的异常UserService.getUserById方法,每5秒输出一次统计结果# 监控getUserById方法,周期5秒
monitor -c 5 com.jam.demo.arthas.service.UserService getUserById
timestamp class method total success fail avg(ms) max(ms)
-----------------------------------------------------------------------------------------------
2024-05-20 15:00:05 com.jam.demo.arthas.service.UserService getUserById 3 2 1 205 300
total(总调用次数)、success(成功次数)、fail(失败次数)、avg(ms)(平均耗时)、max(ms)(最大耗时)。watch [选项] 类名 方法名 表达式 [条件表达式]-x <深度>:指定对象展开深度(默认1,如-x 2展开对象的二级属性)-b:在方法调用前触发(仅显示入参)-e:在方法抛出异常时触发(仅显示异常)-s:在方法返回时触发(仅显示返回值)-f:在方法调用前后都触发(显示入参+返回值)params:方法入参数组(如params[0]表示第一个参数)returnObj:方法返回值throwExp:方法抛出的异常对象target:当前实例对象args:同params,兼容旧版本# 观察getUserById的入参和返回值,对象展开深度2
watch -x 2 -f com.jam.demo.arthas.service.UserService getUserById "{'userId':params[0], 'result':returnObj}"
method=com.jam.demo.arthas.service.UserService.getUserById location=before
args=[1]
returnObj=null
expression={"userId":1, "result":null}
--------------------------------------
method=com.jam.demo.arthas.service.UserService.getUserById location=after
args=[1]
returnObj=User(id=1, username="zhangsan", age=25, email="zhangsan@example.com")
expression={"userId":1, "result":{"id":1, "username":"zhangsan", "age":25, "email":"zhangsan@example.com"}}
# 仅当第一个参数等于1时,显示入参和返回值
watch -x 2 -f com.jam.demo.arthas.service.UserService getUserById "{'userId':params[0], 'result':returnObj}" "params[0]==1"
# 观察方法抛出的异常信息,对象展开深度3
watch -x 3 -e com.jam.demo.arthas.service.UserService getUserById "{'userId':params[0], 'exception':throwExp}"
trace [选项] 类名 方法名 [条件表达式]-d <深度>:指定调用链路深度(默认5,避免递归调用导致栈溢出)-n <次数>:指定追踪次数(如-n 3仅追踪3次调用)-E:开启正则表达式匹配方法名# 追踪getUserById的调用链路,显示每个子方法耗时,仅追踪2次调用
trace -n 2 com.jam.demo.arthas.service.UserService getUserById
`---ts=2024-05-20 15:10:00;thread_name=nio-8080-exec-3;id=10;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@1b6d3586
`---[205ms] com.jam.demo.arthas.service.UserService:getUserById()
`---[0ms] org.springframework.util.ObjectUtils:isEmpty()
`---[1ms] lombok.extern.slf4j.Slf4j:info()
`---[200ms] java.lang.Thread:sleep()
`---[3ms] com.jam.demo.arthas.mapper.UserMapper:selectById()
[205ms]表示总耗时,子方法耗时之和等于总耗时,可快速定位到Thread.sleep()是耗时瓶颈。profiler [选项] [命令]start:开始性能分析(默认CPU维度)stop:停止分析并生成火焰图status:查看分析状态list:列出支持的分析维度-d <时长>:指定分析时长(单位:秒,如-d 60分析60秒)-e <事件>:指定分析事件(如cpu、alloc(内存分配)、lock(锁竞争))-o <格式>:指定输出格式(默认svg,支持html、jfr)# 开始CPU分析,持续60秒
profiler start -d 60
# 60秒后停止分析,生成火焰图(默认保存到arthas-output目录)
profiler stop
# 分析内存分配情况,持续30秒,输出SVG格式
profiler start -d 30 -e alloc -o svg
# 停止分析并生成火焰图
profiler stop
HotSpotDiagnosticMXBean.dumpHeap(filePath, live)接口导出堆快照,live=true表示仅导出存活对象(过滤垃圾对象,减小快照体积)。heapdump [选项] 输出文件路径-live:仅导出存活对象(推荐生产环境使用,减少文件大小)-format=b:指定输出格式为二进制(默认,兼容MAT等分析工具)# 导出存活对象的堆快照到/tmp目录
heapdump -live /tmp/arthas-heap-20240520.hprof
ManagementFactory获取JVM的各种MXBean(如MemoryMXBean、ThreadMXBean、ClassLoadingMXBean),汇总并展示关键指标。jvmJAVA_HOME: /usr/local/jdk-17.0.11
JVM Version: 17.0.11+9-LTS-194
JVM Vendor: Oracle Corporation
JVM Name: OpenJDK 64-Bit Server VM
Input Arguments: -Xms2g -Xmx2g -XX:+UseG1GC
...
Memory Usage:
heap used [786.4MB] total [1.2GB] max [1.8GB] usage[43.7%]
non-heap used [256.3MB] total [272.0MB] max [--] usage[94.2%]
...
Thread Count: 56 (DAEMON: 48, NON-DAEMON: 8)
Peak Thread Count: 62
...
Class Loading:
loadedClassCount: 12345
totalLoadedClassCount: 12345
unloadedClassCount: 0
Instrumentation.redefineClasses(ClassDefinition...)方法,将新的字节码替换旧的类定义,JVM会重新加载类并更新所有实例的方法逻辑。redefine <class文件路径> [多个class文件]final类、final方法、static方法(部分JVM版本)。UserService类(见5.2.3节实例)redefine类似,但支持修改类结构(如新增方法、字段),但需配合自定义的ClassFileTransformer,使用门槛较高。retransform <class文件路径>redefine。ThreadMXBean获取线程信息,包括线程ID、名称、状态、CPU时间、调用栈等。thread [选项] [线程ID]-n <数量>:显示CPU占用率最高的前N个线程-b:查找阻塞其他线程的线程(死锁或锁竞争场景)-i <间隔>:指定刷新间隔(单位:秒,如-i 2每2秒刷新一次)-state <状态>:过滤指定状态的线程(如-state BLOCKED仅显示阻塞线程)thread -n 3
THREAD ID: 10 | NAME: nio-8080-exec-3 | STATE: RUNNABLE | CPU%: 90.5 | TIME: 120s
THREAD ID: 12 | NAME: AsyncTaskExecutor-1 | STATE: RUNNABLE | CPU%: 8.2 | TIME: 30s
THREAD ID: 8 | NAME: GC Thread#0 | STATE: RUNNABLE | CPU%: 1.3 | TIME: 5s
thread -b
Found one blocking thread:
THREAD ID: 15 | NAME: OrderService-Thread-1 | STATE: BLOCKED
Blocked on: com.jam.demo.arthas.service.OrderService@12345678
Blocked by thread: 16 (PaymentService-Thread-1)
Call Stack:
com.jam.demo.arthas.service.OrderService.processOrder(OrderService.java:50)
com.jam.demo.arthas.service.OrderService$1.run(OrderService.java:20)
THREAD ID: 16 | NAME: PaymentService-Thread-1 | STATE: BLOCKED
Blocked on: com.jam.demo.arthas.service.PaymentService@87654321
Blocked by thread: 15 (OrderService-Thread-1)
Call Stack:
com.jam.demo.arthas.service.PaymentService.processPayment(PaymentService.java:30)
com.jam.demo.arthas.service.PaymentService$1.run(PaymentService.java:15)
jstack [选项] 输出文件路径-F:强制导出(当JVM无响应时使用)-l:显示锁相关信息(推荐使用)jstack -l /tmp/arthas-thread-20240520.txt
以下结合生产环境高频问题,完整演示如何使用Arthas排查并解决问题,覆盖CPU飙升、内存泄漏、接口响应慢3类场景,每个案例包含“问题现象->排查步骤->解决方案->优化效果”全流程。
生产环境应用CPU使用率持续高达95%以上,应用响应缓慢,监控告警触发。
top命令找到CPU占用最高的Java进程(PID:12345)。top -p 12345
java -jar arthas-boot.jar 12345
thread -n 1命令找到CPU占用最高的线程(THREAD ID:10)。thread -n 1
THREAD ID: 10 | NAME: ProductSync-Thread-1 | STATE: RUNNABLE | CPU%: 98.7 | TIME: 3600s
thread 10命令查看线程10的调用栈,定位到具体方法。thread 10
Call Stack:
com.jam.demo.arthas.service.ProductSyncService.syncProductData(ProductSyncService.java:45)
com.jam.demo.arthas.service.ProductSyncService$1.run(ProductSyncService.java:25)
java.lang.Thread.run(Thread.java:833)
jad命令反编译syncProductData方法,查看逻辑是否存在死循环。jad com.jam.demo.arthas.service.ProductSyncService syncProductData
public void syncProductData() {
List<Product> productList = productMapper.selectAll();
int i = 0;
// 问题:循环条件错误(i < productList.size() 写成了 i > productList.size())
while (i > productList.size()) {
Product product = productList.get(i);
syncToEs(product);
i++;
}
}
watch命令监控方法执行,确认循环是否无限执行。watch -n 1 com.jam.demo.arthas.service.ProductSyncService syncProductData "{'i':target.i, 'listSize':params[0].size()}"
{'i':0, 'listSize':1000}(i始终为0,循环条件永远成立,导致死循环)。i > productList.size()改为i < productList.size()。redefine命令热更新修复后的类,无需重启应用。# 编译修复后的类
javac -cp "arthas-boot.jar:spring-boot-starter.jar:mybatis-plus-boot-starter.jar:${CLASSPATH}" com/jam/demo/arthas/service/ProductSyncService.java
# 热更新
redefine com/jam/demo/arthas/service/ProductSyncService.class
应用运行一段时间后(约24小时),出现OOM(OutOfMemoryError),堆内存使用率持续上升,无法自动回收。
jvm命令查看堆内存使用情况,发现老年代内存使用率高达99%。jvm
Memory Usage:
heap used [1.7GB] total [1.8GB] max [1.8GB] usage[94.4%]
eden_space used [200MB] total [300MB] max [300MB] usage[66.7%]
survivor_space used [30MB] total [30MB] max [30MB] usage[100%]
old_gen used [1.5GB] total [1.5GB] max [1.5GB] usage[99.8%]
heapdump命令导出存活对象的堆快照。heapdump -live /tmp/arthas-heap-oom.hprof
arthas-heap-oom.hprof文件。com.jam.demo.arthas.service.UserCacheService类的static List<User>集合占用了1.2GB内存,且集合大小持续增长。jad命令反编译UserCacheService类,定位内存泄漏点。jad com.jam.demo.arthas.service.UserCacheService
@Service
public class UserCacheService {
// 问题:静态集合未清理,所有查询的用户对象都被缓存,无法回收
private static final List<User> USER_CACHE = Lists.newArrayList();
/**
* 查询用户并缓存到静态集合
*/
public List<User> queryAllUser() {
List<User> userList = userMapper.selectList(null);
USER_CACHE.addAll(userList); // 只添加不清理,导致集合无限增长
return userList;
}
}
watch命令监控USER_CACHE的大小,确认是否持续增长。watch com.jam.demo.arthas.service.UserCacheService queryAllUser "{'cacheSize':target.USER_CACHE.size()}"
cacheSize均增加(如从1000→2000→3000),确认内存泄漏。<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version> <!-- 最新稳定版 -->
</dependency>
UserCacheService类:package com.jam.demo.arthas.service;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.jam.demo.arthas.entity.User;
import com.jam.demo.arthas.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 用户缓存服务(修复内存泄漏)
* @author ken
*/
@Slf4j
@Service
publicclass UserCacheService {
@Resource
private UserMapper userMapper;
// 缓存配置:过期时间1小时,最大容量10000
privatefinal LoadingCache<String, List<User>> USER_CACHE = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.maximumSize(10000)
.build(key -> userMapper.selectList(null));
/**
* 查询用户(带过期缓存)
*/
public List<User> queryAllUser() {
return USER_CACHE.get("allUser");
}
}
用户查询接口/api/user/query响应时间长达3-5秒,远超正常阈值(500ms),监控显示接口P99延迟过高。
monitor命令监控UserService.queryUserByCondition方法(接口对应的服务方法)。monitor -c 3 com.jam.demo.arthas.service.UserService queryUserByCondition
trace命令查看方法内部子调用的耗时分布。trace -n 5 com.jam.demo.arthas.service.UserService queryUserByCondition
`---[3200ms] com.jam.demo.arthas.service.UserService:queryUserByCondition()
`---[0ms] org.springframework.util.StringUtils:hasText()
`---[3198ms] com.jam.demo.arthas.mapper.UserMapper:selectByCondition()
`---[2ms] com.google.common.collect.Lists:newArrayList()
UserMapper.selectByCondition()方法耗时占比99.9%,是性能瓶颈。jad命令反编译UserMapper接口,获取SQL语句。jad com.jam.demo.arthas.mapper.UserMapper selectByCondition
<select id="selectByCondition" resultType="com.jam.demo.arthas.entity.User">
SELECT id, username, age, email FROM user WHERE age = #{age} AND status = #{status}
</select>
EXPLAIN分析SQL,确认是否缺少索引。EXPLAIN SELECT id, username, age, email FROM user WHERE age = 25 AND status = 1;
type=ALL(全表扫描),key=NULL(未使用索引),rows=100000(扫描10万行数据)。watch命令查看SQL的查询条件和扫描行数(需结合MyBatis-Plus的日志)。watch -x 2 com.jam.demo.arthas.mapper.UserMapper selectByCondition "{'age':params[0].age, 'status':params[0].status}"
age=25、status=1,扫描行数10万+,确认全表扫描导致响应慢。user表添加联合索引(覆盖查询条件):ALTER TABLE `user` ADD INDEX idx_age_status (age, status); -- MySQL 8.0支持
EXPLAIN,确认使用索引。EXPLAIN SELECT id, username, age, email FROM user WHERE age = 25 AND status = 1;
type=ref(索引查找),key=idx_age_status(使用新增索引),rows=100(扫描100行数据)。monitor命令再次监控,确认性能提升。monitor -c 3 com.jam.demo.arthas.service.UserService queryUserByCondition
Arthas支持通过插件机制扩展自定义命令,满足特殊业务场景的监控需求(如自定义指标统计、业务日志采集)。以下是自定义命令的开发步骤:
<dependencies>
<!-- Arthas核心依赖 -->
<dependency>
<groupId>com.taobao.arthas</groupId>
<artifactId>arthas-core</artifactId>
<version>3.7.2</version>
<scope>provided</scope>
</dependency>
<!-- Arthas命令API -->
<dependency>
<groupId>com.taobao.arthas</groupId>
<artifactId>arthas-common</artifactId>
<version>3.7.2</version>
<scope>provided</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
package com.jam.demo.arthas.plugin;
import com.taobao.arthas.core.command.CommandProcess;
import com.taobao.arthas.core.command.annotation.Command;
import com.taobao.arthas.core.command.annotation.Option;
import com.taobao.arthas.core.command.parser.CommandLine;
import com.taobao.arthas.core.shell.cli.Completion;
import com.taobao.arthas.core.shell.cli.CompletionUtils;
import com.taobao.arthas.core.util.ClassLoaderUtils;
import com.taobao.arthas.core.util.ThreadUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 自定义Arthas命令:统计指定用户的订单数
* @author ken
*/
@Slf4j
@Command(name = "count-order", description = "统计指定用户的订单数")
publicclass CountOrderCommand extends com.taobao.arthas.core.command.AbstractCommand {
@Option(name = "-u", longName = "username", required = true, description = "用户名")
private String username;
@Option(name = "-c", longName = "classLoader", description = "类加载器Hash值")
private String classLoaderHash;
privatefinal Map<String, Integer> orderCountCache = new ConcurrentHashMap<>();
@Override
public void execute(CommandProcess process) {
try {
// 1. 获取类加载器
ClassLoader classLoader = ClassLoaderUtils.getClassLoaderByHash(classLoaderHash);
if (classLoader == null) {
classLoader = Thread.currentThread().getContextClassLoader();
}
// 2. 加载OrderService类
Class<?> orderServiceClass = classLoader.loadClass("com.jam.demo.arthas.service.OrderService");
// 3. 获取Spring容器中的OrderService实例(假设使用Spring上下文)
Class<?> applicationContextClass = classLoader.loadClass("org.springframework.context.ApplicationContext");
Method getApplicationContextMethod = classLoader.loadClass("org.springframework.web.context.ContextLoader")
.getMethod("getCurrentWebApplicationContext");
Object applicationContext = getApplicationContextMethod.invoke(null);
Object orderService = applicationContextClass.getMethod("getBean", Class.class)
.invoke(applicationContext, orderServiceClass);
// 4. 调用OrderService的countOrderByUsername方法
Method countMethod = orderServiceClass.getMethod("countOrderByUsername", String.class);
Integer orderCount = (Integer) countMethod.invoke(orderService, username);
// 5. 缓存结果并输出
orderCountCache.put(username, orderCount);
process.write(String.format("用户名:%s,订单数:%d%n", username, orderCount));
} catch (Exception e) {
log.error("统计订单数失败", e);
process.write("统计失败:" + e.getMessage() + "\n");
} finally {
process.end();
}
}
@Override
public void complete(Completion completion) {
CommandLine commandLine = completion.commandLine();
if (commandLine.isOptionCompleted("-u") && commandLine.isOptionCompleted("-c")) {
// 自动补全用户名(假设从数据库查询)
List<String> usernames = Lists.newArrayList("zhangsan", "lisi", "wangwu");
CompletionUtils.completeCandidates(completion, usernames);
}
}
}
arthas-count-order-plugin.jar)。plugins目录(默认~/.arthas/plugins)。# 统计用户zhangsan的订单数
count-order -u zhangsan
root或目标进程所属用户权限,生产环境建议创建专用用户,避免使用root。--username和--password参数)。stop命令卸载代理,避免残留。trace、watch等插桩命令(会增加CPU和内存开销)。profiler时,建议设置较短的采样时长(如30-60秒),避免影响应用性能。~/.arthas/logs目录,生产环境建议定期清理日志文件,避免磁盘空间占用过高。问题现象 | 解决方案 |
|---|---|
无法附着到目标进程(Attach failed) | 1. 确认目标进程的PID正确;2. 确认当前用户有目标进程的访问权限;3. 确认目标JVM版本与Arthas兼容(JDK 6+);4. 关闭目标进程的安全管理器(若启用)。 |
反编译类失败(Decompile failed) | 1. 确认类名正确(包含完整包名);2. 确认类已被JVM加载(通过sc命令验证);3. 若类被混淆,反编译结果可能不完整。 |
热更新失败(Redefine failed) | 1. 检查修改后的类是否与原始类结构兼容(仅修改方法体);2. 确认类未被final修饰;3. 确认JVM支持类重定义(部分嵌入式JVM可能不支持)。 |
火焰图生成失败 | 1. 确认目标JVM支持AsyncProfiler(JDK 8+);2. 检查磁盘空间是否充足;3. 避免在高负载场景下长时间采样。 |
Arthas作为阿里巴巴开源的JVM诊断工具,凭借其“无侵入式、动态增强、功能全面”的特性,已成为Java开发者排查生产环境问题的必备工具。本文从底层原理、核心功能、实战案例三个维度,详细讲解了Arthas的使用方法和最佳实践,覆盖了CPU飙升、内存泄漏、接口响应慢等生产环境高频问题的排查流程。
通过本文的学习,相信读者已经掌握了Arthas的核心使用方法和底层原理。在实际工作中,建议结合具体业务场景,灵活运用Arthas的各类命令,快速定位和解决生产环境问题,提升应用的稳定性和性能。