封面来源:本文封面来源于 Arthas 官网,如有侵权,请联系删除。

官方文档:arthas

本文参考:Java 诊断神器Arthas真有那么香?它到底能解决什么问题 | Arthas 教程实操 | 线上问题排查思路和手段

测试代码:springboot-study/arthas-demo at master · mofan212/springboot-study

1. Arthas 的简介

今天同事问我会不会使用 Arthas 来查看接口的耗时,奈何本人才蔽识浅,仅仅使用过它的 jad 命令来反编译类文件。突然想起曾经收藏过一个关于如何使用 Arthas 的视频,那就利用这个今晚的时间学习下吧。

简介

Arthas 是一款线上监控诊断产品,通过全局视角实时查看应用 Load、内存、GC、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。

诸如此类的工具就和 Mac Book 一样,当你在考虑要不要买 Mac Book 时,那就不要购买;当你不知道 Arthas 有什么用,那就不用学。

能解决什么问题

Arthas 是阿里巴巴出品的线上 JVM 监控诊断利器,它适用于:

  1. 有没有一个全局 JVM 运行时监控?能够显示 CPU、线程、内存、堆栈等信息

  2. CPU 飙高是什么原因造成的?

  3. 接口没反应、卡住了,是不是死锁了?

  4. CTO 说你们这个接口太慢了,要优化一下,如何准确找出耗时的代码?

  5. 写的代码没有执行,是部署的分支不对,还是压根没提交?

  6. 线上有一个低级错误,改起来很简单,能不能在不重启应用的情况下,进行类替换,达到热部署?

2. 快速入门

2.1 安装与启动

进入 下载 | arthas 页面,可以从 Maven 仓库下载,也可以前往 Github Releases 页下载,下载完成后,解压文件。

而在 MacOS / Linux 环境下,可以直接执行以下命令下载:

1
wget https://arthas.aliyun.com/arthas-boot.jar

在启动 Arthas 前,需要有一个 JVM 进程。先运行 main() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SneakyThrows
public static void main(String[] args) {
ArthasDemo demo = new ArthasDemo();
demo.justRun();
}

@SneakyThrows
private void justRun() {
while (Instant.now().isBefore(Instant.now().plus(1, ChronoUnit.DAYS))) {
System.out.println("running");
TimeUnit.SECONDS.sleep(1);
}
}

在有 arthas-boot.jar 的目录下,使用 java -jar 的方式启动:

1
java -jar arthas-boot.jar

运行后会列出所有存在的 Java 进程,找到需要连接的进程:

启动Arthas

之后输入目标进程对应的序号,当界面成功显示 Arthars 的 Banner 时,证明连接成功:

使用Arthas连接到目标进程

2.2 基本命令的使用

help

查看当前 Arthas 版本支持的指令,或查看具体指令的使用说明。

查看当前Arthas版本支持的指令

或者在某一命令后使用 -h 选项,查看该命令的用法和示例,比如:

1
dashboard -h

dashboard命令的使用和示例

根据示例,输入 dashboard,查看 JVM 运行时监控:

使用dashboard命令查看JVM运行时监控

Ctrl + C 退出。

thread

thread 命令可以查看当前线程信息、线程的堆栈。

运行下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SneakyThrows
private void seeThread() {
Thread thread = new Thread(() -> {
System.out.println("this is in a thread");
try {
TimeUnit.HOURS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
thread.setName("thread-demo");
thread.start();
}

利用 Arthas 连接到对应的进程,执行 thread 命令:

执行thread命令

目标线程的序号是 11,运行下列命令查看 11 号线程对应的信息:

11号线程的信息

jad

现在需要打印出一些好习惯:多读书,多看报,少吃零食,多睡觉。

运行下列代码:

1
2
3
4
5
6
@SneakyThrows
private void seeProductionCode() {
GoodHabit goodHabit = new GoodHabit();
goodHabit.doSomething();
TimeUnit.HOURS.sleep(1);
}

控制台打印出:

多读书,多看报

只打印了前半句,这是怎么回事呢?

使用 jad 跟上目标类的全限定名称,反编译目标类:

利用jad命令反编译类

上述反编译结果有两个问题:

  1. doSomething() 方法中只调用了 readAndSleep() 方法,没有其他内容,导致只打印了前半句;
  2. readAndSleep() 方法中的中文乱码。

针对第一个问题,查看源码后发现代码被注释:

1
2
3
4
5
6
7
8
9
10
public class GoodHabit {
public void doSomething() {
readAndSleep();
// System.out.println("少吃零食,多睡觉");
}

private void readAndSleep() {
System.out.println("多读书,多看报");
}
}

放开注释,重新运行程序,控制台打印出:

多读书,多看报
少吃零食,多睡觉

第二个问题的解决则是要在启动 Arthas 时设置 Arthas 向控制台输出内容使用的默认编码:

1
java -Dfile.encoding=UTF-8 -jar arthas-boot.jar

设置使用的默认编码后执行jad输出的内容

2.3 方法的监测

首先推荐一个 IDEA 插件:Arthas Idea,利用该插件可以很方便地生成 Arthas 命令。

watch

watch 命令用于监测方法执行数据。

运行以下代码,循环打印 Car 的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Car getCar(String carName, BigDecimal carPrice) {
Car car = new Car();
car.setName(carName);
car.setPrice(carPrice);
return car;
}

@SneakyThrows
private void printCarInfo() {
for (int i = 0; i < 1000; i++) {
System.out.println(getCar("catName-" + i, new BigDecimal(i)));
TimeUnit.SECONDS.sleep(1);
}
}

此时需要监测 getCar() 方法,鼠标右击目标方法,选择 Arthas Command,然后再选择 Watch 生成 Arthas 命令:

image-20230220225027192

最终生成的命令如下:

1
watch indi.mofan.ArthasDemo getCar '{params,returnObj,throwExp}'  -n 5  -x 3 

这表示:监测 indi.mofan.ArthasDemo 类中的 getCar() 方法,获取方法调用时使用的参数、返回值和异常信息,共监测 5 次,输出的对象属性遍历深度为 3。

如果跟踪的字段或对象过大,会导致输出的内容太多,甚至可能因为控制台的限制而丢失信息。此时可以尝试将 watch 命令的输出重定向到文件中,比如:

1
watch indi.mofan.ArthasDemo getCar '{params,returnObj}'  -n 5  -x 3 > /path/to/output.txt

trace

trace 命令用于获取方法内部调用路径,并输出方法路径上的每个节点上耗时。

利用 Arthas Idea 插件生成相应命令:

1
trace indi.mofan.ArthasDemo getCar  -n 5 --skipJDKMethod false

这表示:监测 5 次 indi.mofan.ArthasDemo 类中的 getCar() 方法,并且不跳过 JDK 中的方法。

利用trace查看方法耗时

执行 trace 命令后,耗时占比最高的部分会高亮显示。

stack

stack 命令用于输出当前方法被调用的调用路径。

利用 Arthas Idea 插件生成相应命令:

1
stack indi.mofan.ArthasDemo getCar -n 5 

这表示:监测 5 次 indi.mofan.ArthasDemo 类中的 getCar() 方法的被调用路径。

利用stack输出方法的被调用路径

monitor

monitor 命令用于方法执行监控。

利用 Arthas Idea 插件生成相应命令:

1
monitor indi.mofan.ArthasDemo getCar -n 10  --cycle 10 

这表示:循环 10 次,每次调用 10 次 indi.mofan.ArthasDemo 类中的 getCar() 方法时的执行信息。

利用monitor监控方法执行

2.4 错误定位

死循环的定位

运行以下代码,这段代码会造成 死循环

1
2
3
4
5
private void deadLoop() {
while (true) {
System.out.println("this is in dead loop");
}
}

使用 dashboard 查看线程信息与内存信息:

使用dashboard命令查看线程信息与内存信息

main() 线程的 CPU 使用率达到 90%,内存中的 nonheapmetaspace 使用率非常高。

再使用 thread -n 3 查看当前 3 个最忙的线程:

利用thread命令查看前三个最忙的线程

死锁的定位

运行以下代码,这段代码会造成 死锁

1
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
41
42
43
private void deadLock() {
Thread thread = new Thread(() -> {
synchronized (A) {
System.out.println("线程一获取到资源A");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// ...
}
System.out.println("线程一尝试获取资源B");
synchronized (B) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// ...
}
}
}
});
thread.setName("死锁一号");
thread.start();

Thread t2 = new Thread(() -> {
synchronized (B) {
System.out.println("线程二获取到资源B");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// ...
}
System.out.println("线程二尝试获取资源A");
synchronized (A) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// ...
}
}
}
});
t2.setName("死锁二号");
t2.start();
}

利用 thread 全局查看线程信息:

利用thread命令全局查看线程信息

可以看到当前有 27 个线程,其中有 2 个线程被阻塞。

还可以利用 thread -b 找出当前阻塞其他线程的线程:

利用thread命令找出当前阻塞其他线程的线程

2.5 时空隧道

tt 命令生成方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测。

记录每次调用方法的环境现场:

1
tt -t 类全限定名 方法名

显示 tt 命令记录的时间片段:

1
tt -l

筛选出目标方法的时间片段:

1
tt -s 'method.name=="目标方法名称"'

查看某次方法的调用信息:

1
tt -i 索引值

其中的索引值为执行 tt -ttt -l 等命令时第一列的值。

tt 命令保存了某次调用的所有现场信息,因此可以重做一次调用:

1
tt -i 索引值 -p

注意事项

tt 命令会将方法、函数的的入参、返回值等信息保存到一个 Map<Integer, TimeFragment> 中,默认大小为 100。

使用 tt 命令后,需要手动释放内存,否则在长时间使用下可能会导致 OOM,并且就算退出 arthas 也不会自动清除 map 缓存。

删除 map 缓存信息的方式:

1
2
3
4
# 通过对应索引删除指定的 tt 记录
tt -d 索引值
# 清除所有的 tt 记录
tt --delete-all

2.6 生成火焰图

profiler 命令可以生成应用热点的火焰图。

启动 profiler

1
profiler start

获取已采集的用例数量:

1
profiler getSamples

查看 profiler 状态(查看当前 profiler 在采样哪种 event 和采样时间):

1
profiler status

默认生成的是 CPU 的火焰图,即 eventcpu,可以使用 --event 指定采样 event

停止采样:

1
profiler stop

停止采样后,默认生成 HTML 格式的结果文件。

3. 奇技淫巧

参考链接:arthas的奇技淫巧-常用高级技巧(spring获取bean,ognl表达式,命令别名,调用方法,对象传参)

3.1 获取 Spring Bean

场景

  1. 执行 Spring 中某个 Bean 的特定方法
  2. 对某段代码做测试,但是当前这个方法不能直接通过接口访问
  3. 不想使用 telent invoke 调用 Dubbo Service 中的代码
  4. 线上观察 Spring 中的 Bean 信息

方法

首先执行以下命令:

1
tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod -n 1

上述命令监听的是 MVC 的请求映射处理器适配器,因此所有接口的调用都会触发。-n 用于指定记录次数,不指定可能导致 OOM。

执行上述命令后访问任意接口,成功后会退出 tt 命令的监听(因为 -n 1 表示只监听一次),并输出一个 INDEX,第一次为 1000。返回执行上述命令,INDEX 会递增。

之后执行下述命令获取 Spring 容器中的 Bean 并调用对应方法:

1
tt -i 1000 -w 'target.getApplicationContext().getBean("Bean 名称").方法名()'

-w 选项表示使用 OGNL 表达式监听对应的方法。

3.2 调用静态方法

调用静态方法需要使用到 ognl 命令,用于执行 OGNL 表达式。

调用静态方法:

1
2
3
ognl '@java.util.Objects@hashCode(1)'
ognl '@java.lang.Thread@currentThread()'
ognl '@java.util.ArrayList@DEFAULT_CAPACITY'

调用实例方法:

1
2
ognl '@java.lang.System@out.println("hello world")'
ognl '@java.lang.Runtime@getRuntime().gc()'

使用 ognl 命令的规则:

  • 定位类:@类的全限定名,比如 @java.util.Objects
  • 定位类的 Class 对象:@类的全限定名@class,比如 @java.util.Objects@class
  • 定位类的静态方法或静态字段:@类的全限定名@方法名或字段名,比如 @java.lang.Thread@currentThread()
  • 定位实例方法或实例字段:@类的全限定名.方法名或字段名,比如 @java.lang.Thread@currentThread().name

3.3 传递对象参数

使用 tt 命令监听方法或使用 ognl 调用静态方法时,String 类型或者数字类型的参数可以直接传入,但对象类型的参数不能直接传参,也不支持以 JSON 的形式传递。

可以 灵活变通下,使用 ognl 命令将 JSON 字符串反序列化为对象,然后传入对应的方法即可。

比如:

1
tt -i 1000 -w 'target.getApplicationContext().getBean("Bean 名称").方法名(@com.alibaba.fastjson.JSON@parseObject("{\\"name\\":\\"mofan\\",\\"age\\":23}", @com.example.Person@class))'

这里使用 FastJson 将以下 JSON 字符串转换为 Person 对象:

1
2
3
4
{
"name": "mofan",
"age": 23
}

3.4 查看内存数据

使用 vmtool 命令利用 JVMTI 接口,实现查询内存对象,强制 GC 等功能。

获取对象:

1
vmtool --action getInstances --className com.example.SpringBootApplication --limit 1

执行方法:

1
vmtool --action getInstances --className com.example.SpringBootApplication --limit 1 --express 'instances[0].方法名(参数)'

tt 命令的区别:

  • tt 命令可以直接获取 Spring 中的 Bean,当知道 Bean 的名称时,可以直接通过其名称获取到 Bean 对象;
  • 如果需要获取的对象没有被 Spring 管理,那么就可以使用 vmtool 命令。

tt 一样使用 -n 选项类似,使用 vmtool 命令时也需要注意使用 --limit 选项。

4. 学习资源

第一学习资源当然是官网 arthas,官网除了提供 详尽的文档 外,还提供了 在线教程,让用户在实践中学习。

除此之外,在 Github 的 arthas 托管仓库中,Issues 中有个名为 user-case 的 Labels,这下面记录了 Arthas 的使用实例与最佳实践。