简明的Arthas配置及基础运维教程
# 写在文章开头
Arthas是一款强大的开源Java诊断程序,它可以非常方便的启动并以界面式的方式和Java程序进行交互,支持监控程序的内存使用情况、线程信息、gc情况、甚至可以反编译并修改现上代码等。所以它成为笔者进行线上问题排查的重要手段,而本文将从实际使用的角度介绍一下arthas的基本使用技巧,希望对你有帮助。
我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。
# Arthas基础说明与配置演示
# Arthas的运行原理(理解)
arthas的运行原理大致是以下几个步骤:
- 启动
arthas选择目标Java程序后,arthas会向目标程序注入一个代理。 - 代理会创建一个集
HTTP和Telnet的服务器与客户端建立连接。 - 客户端与服务端建立连接。
- 后续客户端需要监控或者调整程序都可以通过服务端
Java Instrumentation机制和应用程序产生交互。

# 下载安装
文章开始前,我们需要先下载安装一下Arthas,Arthas的官方地址为:https://arthas.aliyun.com/ (opens new window)
考虑到方便笔者一般是使用命令行的方式下载:
curl -O https://arthas.aliyun.com/arthas-boot.jar
2
完成后我们通过下面jar命令将Arthas启动:
java -jar arthas-boot.jar
此时我们就可以看到对应的进程序号和进程的pid,以笔者为例,开启arthas之后就会看到一个序号为1的9121的Java进程,我们可以直接点击1并输入回车对此进程进行监控管理:
[INFO] JAVA_HOME: /usr/lib/jvm/java-8-openjdk-amd64/jre
[INFO] arthas-boot version: 3.7.2
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 9121 arthasExample.jar
2
3
4
随后arthas初次会进行相关依赖下载,然后我们就可以正式的使用arthas管理当前进程了:

# 离线用户的使用姿势(可选阅读)
考虑到内网用户无法联网进行arthas初始化,所以arthas也人性化的提供了全量包的下载方式,有需要的读者可移步到下载页面选择全量包下载即可获取全量的arthas包:

完成后下载并解压之后,我们可以直接通过下面这个脚本指令快速启动arthas:
./as.sh
# 常见指令介绍
步入arthas我们就可以进行一些比较基础的操作,以下是笔者日常用的比较多的指令,和Linux差不多,读者可自行参阅了解
cat:打印文件内容。cls:清空当前屏幕区域内容。grep:匹配查找。history:打印历史命令。pwd:输入当前Java进程所在的位置。quit:退出当前arthas客户端。stop:关闭arthas服务端,所有arthas客户端都会退出。
这里笔者就简单的演示一下,可以看到pwd输出的就是笔者所监控的Java进程所处的文件目录:
[arthas@9121]$ pwd
# 当前监控的进程在服务器上的目录
/home/sharkchili/arthasExample
[arthas@9121]$
2
3
4
又比如笔者通过memory查看堆内存使用情况,如果只想看老年代的数据,就可以使用grep:
memory |grep ps_old_gen
点击quit会直接退出当前进程的客户端,stop同理只不过多是连着服务端和其他客户端一并杀掉,这里就不多做演示了:
[arthas@9121]$ quit
# 直接返回到服务器的目录
sharkchili@DESKTOP-7IPKPVJ:~/arthas$
2
3
4
5
# 快捷启动配置
为了快捷启动arthas,笔者也给出个人的配置方式,首先vim一个名为as.sh的脚本,其内容为arthas-boot.jar的启动指令:
java -jar /home/sharkchili/arthas/arthas-boot.jar
完成脚本编写确认启动无误之后,我们将这个脚本通过alias重命名的方式追加到/etc/profile下,内容如下即sh指令加上上述脚本的全路径:
alias as="sh /home/sharkchili/arthas/as.sh"
2

然后通过source指令使其生效即可。对应mac用户也是同理,指定下载arthas的jar包到指定路径后,编辑相同的启动脚本添加到~/.zshrc并通过source使之生效,以笔者为例则是:
alias as="/Users/sharkchili/arthas/as.sh"
可以看到完成这样一段配直接后,我们可以直接通过as指令完成arthas的快捷启动:

# Arthas中比较常用的运维指令
# 查看实时数据面板
日常开发维护过程中对于项目的巡检还是蛮重要的,通过Arthas的dashboard可以非常直观的查看当前系统中进程的运行情况。
在arthas的控制面板输入dashboard,默认情况下5s进行一次刷新:

这里我们来简单介绍一下第一板块线程中的字段的含义:
ID:java级别的线程id号,注意与jstack中的native id的区别。NAME:线程名称。GROUP:线程所在线程组名。PRIORITY:线程优先级,值越大优先级越高。STATE:线程运行状态。CPU%:线程CPU使用率。DELTA_TIME:上次采样之后,线程运行的CPU时间,单位为秒。TIME:线程运行的总CPU时长,数据格式为分:秒。INTERRUPTED:线程当前的中断位状态。DAEMON:是否为守护线程。
以本次输出位列,可以看到1号线程(一般都是main线程)信息为;
- group为main线程组
- 优先级为5也就是默认优先级
- 线程处于运行也就是runnable状态
- 本次采集到的CPU使用率为0
- 因为是第一次采集且刚开始采集,所以
DELTA_TIME运行时间为8s - 中断标志为为false
- main线程非守护线程

第二板块就是内存使用版块,记录各个堆区、元空间的内存使用情况以及GC情况,以笔者为例,当前项目使用jdk版本为8且使用的回收器对应新生代是Parallel Scavenge收集器 + 老年代PS MarkSweep (其实跟Serial old共用同一份代码),所以对应几个比较常用的参数含义分别是:
- 堆内存81M已使用30M
- 新生代eden区31M已用14M,而survivor则是5M用了4M
- 老年代45M已用11M
- 元空间45M基本用完
- 新生代触发3次gc耗时19ms
- 老年代触发1次耗时21ms

而第三板块则是服务器运行参数版块,这一版块记录着程序当前运行服务器的内核版本信息、jdk版本等,以笔者为例则是Linux系统和jdk8版本:

需要了解的是arthas中的操作指令可以通过--help了解查阅,我们以dashboard为例,其使用说明如下,可以看到我们可以通过-i决定面板刷新间隔(单位是毫秒),用-n决定面板刷新次数:

所以如果我们希望每1s刷新1次,刷新5次,那么对应的命令就是:
dashboard -i 1000 -n 5
# 查看JVM信息
arthas也可能非常直观的查看jvm信息,对应的指令也就是jvm。
同样的这个指令也会输出多个板块的内容,我们先来看看第一个板块,可以可以看到该指令可以非常直观的看到机器名称、jvm启动时间、jdk版本以及我们配置jvm参数信息:

由于板块比较多,这里笔者就说几个笔者比较常用的板块,分别是线程板块和文件描述符板块,通过这两个板块笔者可以日常巡检了解是否发生线程死锁或者程序中是否出现资源未能及时关闭的情况:
THREAD:它记录当前活跃线程数、活跃的守护线程数、从JVM启动开启曾经活着的最大线程数、总共启动线程数以及发生死锁的线程数。FILE-DESCRIPTOR:这个板块记录JVM可以打开的最大文件描述符和当前已经打开的文件描述符数。

# 查看和修改日志级别
logger指令也算是笔者比较喜欢的指令,它可以非常直观的查看我们对于日志的配置,如下图,以笔者当前运行的程序为例,可以看到如下几个信息:
- 日志级别为
INFO。 - 存储错误日志的
ERROR_FILE的相对路径。 - 存储普通日志
INFO_FILE的相对路径。

当然我们也可以查看指定名字的日志信息,例如我们想查看com.example.arthasExample.TestController的日志信息,就可以直接键入logger -n com.example.arthasExample.TestController指令进行查看:

logger指令还有一个比较实用的功能,即直接修改日志级别,例如我们希望修改ROOT这个名称的日志级别,就可以基于如下步骤完成修改:
- 获取
classLoader的哈希码。 - 基于哈希码通过
logger指令修改日志级别。
我们程序中有这样一段代码,此时我们请求下面这个接口只会输出info级别的日志:
@GetMapping(value = {"/user/logger"})
public String loggerPrint() {
log.info("info logger");
log.debug("debug logger");
return "success";
}
2
3
4
5
6
对应的输出结果为:
2024-08-19 23:53:29.454 INFO c.e.a.TestController :138 http-nio-8080-exec-1 info logger
2
接下来我们就直接通过arthas修改日志级别,首先我们需要获取当前classloader的哈希码:
sc -d com.example.arthasExample.TestController

然后我们直接通过这个哈希码,执行如下指令将日志设置为debug
logger -c 306a30c7 --name ROOT --level debug
于是debug日志就出现了:
2024-08-20 00:13:31.438 INFO c.e.a.TestController :138 http-nio-8080-exec-6 info logger
2024-08-20 00:13:31.439 DEBUG c.e.a.TestController :139 http-nio-8080-exec-6 debug logger
2
3
# 查看JVM内存信息
接下来就是memory指令,这也是笔者比较常用的指令之一,通过memory我们可以监控到当前内存的使用情况。如下图所示,键入memory指令后我们就可以看到这些区域的内存已用、总大小、最大值以及使用率等信息:

对应的我们也给出上文中memory各行代表的含义:
heap:堆区内存。ps_eden_space:堆内存中新生代Eden区。ps_survivor_space:堆内存中新生代survivor区。ps_old_gen:堆内存老年代区。nonheap:非堆内存,即堆内存之外的内存。code_cache:因为Java执行是将字节码编译为机器码,而这个区域就是用于缓存这部分代码。metaspace:元空间,以jdk1.8为例该空间是用于存储Java类和方法的元数据信息、常量池等。compressed_class_space:存放类文件信息的区域。
对应的我们在这里也简单的复习一下JVM内存区域的分布,建议读者可参考下图了解memory指令中各个字段的含义:

# 查看JVM的环境变量
arthas的sysenv指令常用于获取系统环境变量信息,键入这条指令我们可以看到当前java程序所使用的系统大部分环境变量信息,如下图,可以看到大部分的当前系统用户名称、编码格式、当前程序路径以及客户端ip和端口号等信息:

根据help的提示,这条指令同样也支持查询单个环境变量,不过意义不大,毕竟不是每个都知道环境变量叫什么,只有查看了才知道(笑):
[arthas@23543]$ sysenv PWD
KEY VALUE
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
PWD /home/sharkchili/arthasExample
2
3
4
# 查看和修改JVM系统属性
sysprop查看的jvm的系统属性,基本上通过这个指令我们可以看到大部分的JVM参数配置信息,如下输出结果,我们大体可以看到JDK版本、程序名称以及日志编码格式和当前系统用户名称等:

# 查看当前JVM线程堆栈信息
arthas提供thread指令用于查看线程情况,如下所示,它基本打印了线程计数信息和几个活跃的线程的实时情况,默认情况下,它是按照CPU增量时间降序进行排序:

按照help的提示,我们也可以通过-n打印前几个忙碌的线程调用堆栈信息,如下所示,笔者希望打印出前2条忙碌的线程,键入的指令为thread -n 2:

同时arthas也支持按照时间间隔进行输出打印,比如我们希望列出5s内最忙的3个线程,那么对应的指令就是:
thread -n 3 -i 5000
当我们的Java程序有大量的线程时候,我们希望筛选中某种状态的线程,我们可以通过--state指定,例如我们希望打印处于RUNNABLE状态的线程,那么我们就可以键入thread --state RUNNABLE来获得输出结果:

对于死锁问题,我们也可以通过-b指令来定位查看当前程序是否存在阻塞其他线程的线程,如下图所示,以笔者为例当前程序就不存在死锁的情况:

此时我们给出一个触发死锁的接口并调用:
@RequestMapping("dead-lock")
public void deadLock() {
//线程1先取得锁1,休眠后取锁2
new Thread(() -> {
synchronized (lock1) {
try {
log.info("t1 successfully acquired the lock1......");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
log.info("t1 successfully acquired the lock1......");
}
}
}, "t1").start();
//线程2先取得锁2,休眠后取锁1
new Thread(() -> {
synchronized (lock2) {
try {
log.info("t2 successfully acquired the lock2......");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
log.info("t2 successfully acquired the lock1......");
}
}
}, "t2").start();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
此时,键入-b指令就可以定位阻塞其他线程的线程以及所在代码段:

# vmtool对于JVM的调控
vmtool算是笔者用的比较多的一个工具指令,可用于查询对象或者强制GC等功能,这些功能读者可自行参考官网查阅:
vmtool:https://arthas.aliyun.com/doc/vmtool.html (opens new window)
而笔者这里想要介绍的是一个强制打断线程的功能,这个指令对于特定场景下应急处理还是蛮实用的。
我们的程序调用下面的接口被系统监控到CPU100%,此时我们就可以通过arthas进行特定场景下的应急处理:
@RequestMapping("cpu-100")
public static void cpu() {
//无限循环输出打印
new Thread(() -> {
while (true) {
log.info("cpu working");
}
}, "thread-1").start();
}
2
3
4
5
6
7
8
9
我们通过thread指令看到thread-1基本将单个CPU跑满了,并且我们通过控制台定位到对应的id为48:

此时我们可以通过vmtool的action指令将线程打断:
vmtool --action interruptThread -t 48
完成操作之后即可看到这个线程被我们成功打断了:

同时vmTool也支持观测变量的详情,以下面这个实例变量dateTimeStr 为例,每次接口请求都会实时刷新:
private String dateTimeStr = DateUtil.formatDateTime(new Date());
@RequestMapping(value = "/getVal")
public String getVal() {
dateTimeStr = DateUtil.formatDateTime(new Date());
log.info("dateTimeStr: {}", dateTimeStr);
return "success";
}
2
3
4
5
6
7
8
如果我们希望查看此刻dateTimeStr 的值,我们就可以通过vmtool的action指定为getInstances ,然后指定类的全路径(以笔者这段代码为例则是com.example.arthasExample.TestController),最后键入表达式instances[0].dateTimeStr意为获取当前实例的dateTimeStr:
vmtool --action getInstances --className com.example.arthasExample.TestController --express 'instances[0].dateTimeStr'
此时我们就可以非常直观的监控到这个变量的信息了:

# 常见的运维操作示例
# 反编译查看代码
上文我们其实已经用到了jad这个反编译命令,对于笔者来说,jad有两种比较常见的用法,除了上述那种反编译类的指令jad --source-only 类的包路径,还有一种定位方法代码段的命令jad --source-only 类的包路径 方法名。
例如笔者想定位TestController的deadLock代码,我们就可以键入:
jad --source-only com.example.arthasExample.TestController deadLock
如下图,我们可以直接看到的方法的代码:

# 查看字段详情
我们希望看到某个类下所有字段的详情,我们就可以使用这条命令
sc -d -f 类的包路径
例如笔者想查看TestController的字段详情,就可以键入这条命令:
sc -d -f com.example.arthasExample.TestController
可以看到这条指令不仅可以查看字段的定义和注解,还可以查看线程池的使用情况以及集合内部的value:

# 查看方法列表
这条命令笔者不是很常用,为了教程的完整性笔者也演示一下,假如我们希望查看某个类的方法,可以使用:
sm 类的包路径
以笔者为例,查看TestController的方法为:
sm com.example.arthasExample.TestController
输出结果如下:

# 静态变量监控
Arthas提供了对静态变量的分析,以下面这段代码为例,如果笔者希望看到list 内部详情,我们就可以使用ognl表达式获取:
private static List<String> list = new ArrayList<>();
@RequestMapping("add")
public void add() {
for (int i = 0; i < 10; i++) {
list.add("val" + i);
}
}
2
3
4
5
6
7
8
9
在我们执行完接口完成添加操作之后,我们可以直接使用ognl进行监控。例如我们希望查看上述list的内容,我们就可以使用命令:
ognl '@类的包路径@变量名'
所以如果笔者查看list的命令为:
ognl '@com.example.arthasExample.TestController@list'
注意,某些场景下使用ongl表达式查看字段信息需要指明classLoader的hashCode,否则会报错,所以我们需要通过sc指令先获取当前类的hashCode:
sc -d com.example.arthasExample.TestController
如下图TestController的类加载器的哈希码为68de145:

于是对应的指令我们可以改为ognl -c 68de145 '@com.example.arthasExample.TestController@list',而输出结果如下,可以看到变量的类型和数值都可以看到:

当然ognl还有一些比较特殊的用法,例如查看集合的长度,添加元素到集合中等操作,具体可以参考GitHub这个issue:
https://github.com/alibaba/arthas/issues/71:https://github.com/alibaba/arthas/issues/71 (opens new window)
# 基于Arthas进行问题诊断
# 定位CPU 100%问题
CPU 100%问题算是生产环境下最难排查的问题了,在没有Arthas之前,我们常规的排查思路大致为:
- 使用
top确定是否为java进程。 - 明确是当前
java进程之后,通过jps定位java进程号。 - 找到最吃资源的线程号。
- 将线程号转为十六进制。
- 通过
jstack导出日志并使用全局搜索十六进制线程号定位到问题代码段。
有了Arthas之后,问题的定位就会简单快速许多,为了演示这个例子,笔者编写了一段模拟CPU 100%问题的代码段:
private static ExecutorService threadPool = Executors.newCachedThreadPool();
@RequestMapping("cpu-100")
public static void cpu() {
//无限循环输出打印
new Thread(() -> {
while (true) {
log.info("cpu working");
}
}, "thread-1").start();
}
2
3
4
5
6
7
8
9
10
11
12
我们不断请求该地址,不久后你就会发现CPU直接飙升接近100%,此时我们的arthas就派上用场了,首先我们自然是将arthas启动。
java -jar arthas-boot.jar
此时控制台会出现下面几个选项,它通过不同序号标明不同的Java程序,我们看到我们的目标程序ArthasExampleApplication,序号为1,所以我们输入1按回车。
F:\github>java -jar arthas-boot.jar
[INFO] JAVA_HOME: D:\myinstall\jdk8\jre8
[INFO] arthas-boot version: 3.6.9
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 18720 com.example.arthasExample.ArthasExampleApplication
[2]: 19300 org.jetbrains.jps.cmdline.Launcher
[3]: 7876 org.jetbrains.idea.maven.server.RemoteMavenServer
[4]: 14488
2
3
4
5
6
7
8
进入控制台,我们直接键入thread命令可以看到,有一个名为 pool-1-thread-1的线程CPU使用率非常高,所以我们需要定位它所工作的代码段。

由控制台可知,它的序号为59,所以我们直接键入:
thread 59
很快的,我们直接定位到了问题代码段,在TestController的42行。

知道了代码的位置之后,我们根据类的包路径com.example.arthasExample.TestController直接通过Arthas反编译查看源码,命令如下:
jad --source-only com.example.arthasExample.TestController
最终我们定位到了问题代码,即时修复即可:

# 定位线程死锁问题
对于线程死锁问题,我们也给出下面这样一段示例代码,线程1先取锁1再取锁2,线程2反之,两者取锁顺序构成环路造成死锁。
//两把锁
private Object lock1 = new Object();
private Object lock2 = new Object();
@RequestMapping("dead-lock")
public void deadLock() {
//线程1先取得锁1,休眠后取锁2
new Thread(() -> {
synchronized (lock1) {
try {
log.info("t1 successfully acquired the lock1......");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
log.info("t1 successfully acquired the lock1......");
}
}
}, "t1").start();
//线程2先取得锁2,休眠后取锁1
new Thread(() -> {
synchronized (lock2) {
try {
log.info("t2 successfully acquired the lock2......");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
log.info("t2 successfully acquired the lock1......");
}
}
}, "t2").start();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
了解代码流程之后,我们直接调用这个接口,打开Arthas查看键入thread线程信息可以看到我们的t1和t2两个线程处于等待状态,大概率存在问题。

随后我们直接键入thread -b,发现t2线程被锁住了,由此断定这两个线程看定存在死锁,

由上述结果我们可知两个线程的id分别是65和66,所以使用thread id号的命令直接定位到问题代码段并解决问题即可。

# 运行耗时性能问题排查
对于统计耗时的操作我们经常会用打日志的方式来监控,在环境复杂的生产环境,我们常因为欠缺考虑而忘记对某些方法进行监控。
同样的Arthas也为我们提供了一些便捷的命令来完成对方法耗时的监控与统计。
这里笔者这里给出一段UserServiceImpl 模拟用户查询时进行参数校验、其他service调用、redis调用、MySQL调用。
@Service
@Slf4j
public class UserServiceImpl {
public JSONObject queryUser(Integer uid) throws Exception {
check(uid);
service(uid);
redis(uid);
return mysql(uid);
}
public void service(Integer uid) throws Exception {
log.info("调用其他service。。。。。");
TimeUnit.SECONDS.sleep(RandomUtil.randomLong(1, 2));
}
public void redis(Integer uid) throws Exception {
log.info("查看redis缓存数据。。。。。");
TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(100, 200));
}
public JSONObject mysql(Integer uid) throws Exception {
log.info("查询MySQL数据......");
TimeUnit.SECONDS.sleep(RandomUtil.randomInt(3, 4));
JSONObject jsonObject = new JSONObject();
jsonObject.putOnce("name", "xiaoming");
jsonObject.putOnce("age", 18);
return jsonObject;
}
public boolean check(Integer uid) throws Exception {
if (uid == null || uid < 0) {
log.error("非法用户id,uid:{}", uid);
throw new Exception("非法用户id");
}
return true;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
对应的controller代码如下,假如我们在生产环境下发现这个接口非常耗时,我们又没有日志,那么我们如何利用Arthas排查耗时问题呢?
@Autowired
private UserServiceImpl userService;
@GetMapping(value = "/user")
public JSONObject queryUser(Integer uid) throws Exception {
return userService.queryUser(uid);
}
2
3
4
5
6
7
8
9
我们可以用trace命令,我们首先用trace追踪一下TestController 的queryUser耗时的调用:
trace com.example.arthasExample.TestController queryUser
可以发现TestController 并无异常,所有的耗时都在UserServiceImpl :

所以我们再对UserServiceImpl 的queryUser进行追踪:
trace com.example.arthasExample.UserServiceImpl queryUser
完成命令键入后,控制台就会阻塞监控这个方法,然后我们调用一下这个接口,可以发现原来是MySQL查询非常耗时,由此我们就可以进一步去推断问题了。

# 方法耗时统计
有时候我们希望监控某个方法单位时间内请求的耗时和调用情况,我们就可以使用monitor命令,例如我们希望每5s查看TestController 的queryUser的情况,我们就可以键入:
monitor -c 5 com.example.arthasExample.TestController queryUser
可以看到控制台会每5s输入请求次数、成功和失败次数以及平均耗时等信息。

# 定位出入参错误问题
有时候我们希望定位某个日志没有打到的方法的出入参详情,例如上面的mysql()的出入参,我们完全可以通过Arthas的watch方法做到,对应命令为:
watch com.example.arthasExample.UserServiceImpl mysql '{params[0],returnObj}'
可以看到,我们的入参为1,出参是一个对象。

更进一步,假如我们希望可以打印出对象的内容,那么我们就可以使用toString方法做到
watch com.example.arthasExample.UserServiceImpl mysql '{params[0],returnObj.toString()}'

除此之外watch 还支持很多的骚操作,具体可以参考官方文档:
https://arthas.aliyun.com/doc/watch.html:https://arthas.aliyun.com/doc/watch.html (opens new window)
# 监控方法调用路径
还是以上文mysql方法为例,如果我们希望快速定位到它的调用路径,我们可以使用stack方法:
stack com.example.arthasExample.UserServiceImpl mysql
可以看到详细的调用路径直接输出到控制台。

# 获取方法调用的过程
我们希望查看方法调用时出现异常的原因,出参和入参时,可以使用tt这条指令,例如我们想查看check方法为何会报错,我们就可以使用tt
tt -t com.example.arthasExample.UserServiceImpl check
从输出结果来看,第二次抛出异常了,我们可以基于1001这个索引去定位问题。

tt -i 1001
最终可以得出,入参为-1:

如果我们想重新发起调用,可以直接使用:
tt -i 1001 -p
需要强调的是,tt 命令是将当前环境的对象引用保存起来,但仅仅也只能保存一个引用而已。如果方法内部对入参进行了变更,或者返回的对象经过了后续的处理,那么在 tt 查看的时候将无法看到当时最准确的值。这也是为什么 watch 命令存在的意义。
所以使用完tt指令后,我们建议清除所有的 tt 记录:
tt --delete-all
# 内存溢出问题
以下面这段代码为例,通过不断发起请求调用,复现OOM问题:
final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100, 100, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>());// 创建线程池,通过线程池,保证创建的线程存活
@RequestMapping(value = "/oom")
public String oom(HttpServletRequest request) {
poolExecutor.execute(() -> {
Byte[] c = new Byte[4* 1024* 1024];
localVariable.set(c);// 为线程添加变量
});
return "success";
}
2
3
4
5
6
7
8
9
10
11
12
然后通过arthas发现老年代内存几乎已满,gc也十分频繁。

由此我们可以直接使用Arthas的heapdump 导出文件到mat中进行进一步分析。
heapdump D://heap.hprof
导出的结果如下,后续我们就可以点击detail推断到问题的源头了。

最终我们很快速的定位到了问题代码:

# Arthas的一些进阶操作
# 线上替换代码
有时候我们测试难免会遗漏一些情况,如下所示,我们业务要求id小于1才抛出异常,但是我们因为粗心而将判断条件写成id<2,结果懵懵懂懂的就将这段代码部到了生产环境,导致业务查询出了问题。
@GetMapping(value={"/user/{id}"})
public JSONObject findUserById(@PathVariable Integer id) {
log.info("id: {}",id);
if (id != null && id < 2) {
throw new IllegalArgumentException("id < 1");
}
return new JSONObject().putOnce("name","user"+id).putOnce("age",18);
}
2
3
4
5
6
7
8
对于生产环境,我们肯定是无法立刻重启替换jar包的,对于这类问题,我们完全可以使用arthas实现在线热更新。
首先第一步,我们将生产环境的字节码反编译并导出到本地,如下所示
jad --source-only com.example.arthasExample.TestController > d://TestController.java
然后我们修改一下对应的代码段

然后我们需要找到这个类对应类加载器的hash码
sc -d *TestController | grep classLoaderHash

找到对应hash码之后,我们就可以基于这个类加载器将修改后的Java文件编译成字节码文件:
mc -c 18b4aac2 d://TestController.java -d d://
最后我们将代码热更新到正在运行的程序:
redefine d://com/example/arthasExample/TestController.class
此时我们传1作为参数就不会报错了,说明代码热更新成功了。

# 获取spring上下文进行进一步分析操作
有时候我们希望在线上获取Spring容器分析定位问题,我们完全可以通过arthas拦截到这个类并进行进一步调用和分析。
读过Spring MVC源码的读者可能都知道,每当又HTTP请求发送到web容器时都会进行请求进行映射转发处理时都会经过RequestMappingHandlerAdapter这个适配器上,查看RequestMappingHandlerAdapter的类图,可以看到它继承了WebApplicationObjectSupport,而该类有一个方法getApplicationContext它就可以直接获得Spring上下文,所以接下来笔者就演示如何通过tt指令获得RequestMappingHandlerAdapter从而调用getApplicationContext拿到上下文,开始各种骚操作。

首先我们使用tt指令对RequestMappingHandlerAdapter 进行追逐,如下所示,笔者通过-t知名要追踪的类的全路径和方法,后续只要有请求调用该方法,tt指令就会显示追踪的信息:
tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod
2
然后我们随便调用一个接口,得到调用记录:

我们就以索引为1000的调用记录,通过-w指定ognl获取到spring上下文并获取到testController然后完成一个方法调用:
tt -i 1000 -w 'target.getApplicationContext().getBean("testController").findUserById(3)'
如下图,可以看到,我们成功的完成了调用并得到了返回结果。

# 常见问题
# arthas统计方法耗时watch指令的原理是什么
watch指令示例如下:
watch com.example.MyService sayHello "{params, result, throwExp} -> { return 'Exception: ' + throwExp; }" -E
我们从指令即可看出他可以获取到类的全路径,对此我们不拿理解,它就是基于这个类的全路径定位到类的全路径,并对该类型通过字节码桩技术对类的方法进行增强,即插入一段代码进行在方法执行前后插入时间,实现耗时统计:

# 小结
自此我们对Arthas的常见操作都已演示完成,更多关于Arthas的使用读者可以参考Arthas官网,希望对你有帮助:
https://arthas.aliyun.com/doc/:https://arthas.aliyun.com/doc/ (opens new window)
我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis (opens new window)。
# 参考
Arthas官网:https://arthas.aliyun.com/doc/ (opens new window)
Arthas基础(一):https://blog.csdn.net/wfw123123/article/details/120010941 (opens new window)
Arthas - Java 线上问题定位处理的终极利器| 8月更文挑战: https://juejin.cn/post/6993130443654725668#heading-24 (opens new window)
Arthas排查jvm相关问题的利器: https://www.qinglite.cn/doc/439664780602670ae (opens new window)
新的开始 | Arthas GitHub Star 破万后的回顾和展望:https://juejin.cn/post/6844903783244234765#heading-3 (opens new window)
阿里巴巴Arthas实践--jad/mc/redefine线上热更新一条龙:https://mp.weixin.qq.com/s?__biz=MzkxNDI0ODE0NQ==&mid=2247483937&idx=1&sn=177cd8e97428a053c6365d24aee94c4b&source=41#wechat_redirect (opens new window)
Alibaba Arthas实践--获取到Spring Context,然后为所欲为:https://mp.weixin.qq.com/s?__biz=MzkxNDI0ODE0NQ==&mid=2247483935&idx=1&sn=af7eabb4b6c39f362acb6eebeb7ed8b0&source=41#wechat_redirect (opens new window)
深入探究 JVM | 探秘 Metaspace:https://www.sczyh30.com/posts/Java/jvm-metaspace/ (opens new window)
JVM运行时内存 :https://blog.csdn.net/m0_53611007/article/details/120685628 (opens new window)
Java诊断神器Arthas:https://blog.csdn.net/sundehui01/article/details/121921481 (opens new window)
PS MarkSweep 和Serial Old 的区别:https://www.jianshu.com/p/624338a8d51b (opens new window)