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

官方文档:arthas

本文参考:JVM诊断调优工具Arthas 教程到实战(CPU飙高,线程死锁,请求回放,方法监控)

测试代码: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。

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

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,官网除了提供 详尽的文档 外,还提供了 在线教程,让用户在实践中学习。

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