Arthas是一款强大的开源Java诊断程序,它可以非常方便的启动并以界面式的方式和Java程序进行交互,支持监控程序的内存使用情况、线程信息、gc情况、甚至可以反编译并修改现上代码等。
一、简述Arthas的运行原理(理解)
arthas的运行原理大致是以下几个步骤:
- 启动arthas选择目标Java程序后,artahs会向目标程序注入一个代理。
- 代理会创建一个集HTTP和Telnet的服务器与客户端建立连接。
- 客户端与服务端建立连接。
- 后续客户端需要监控或者调整程序都可以通过服务端Java Instrumentation机制和应用程序产生交互。
二、详解Arthas基础使用
1.下载安装
在介绍几个典型的案例之前,我们需要先下载安装一下Arthas,Arthas的官方地址如下:
Arthas的官方地址:https://arthas.aliyun.com/
考虑到方便笔者一般是使用命令行的方式下载:
curl -O https://arthas.aliyun.com/arthas-boot.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
随后arthas初次会进行相关依赖下载,然后我们就可以正式的使用arthas管理当前进程了:
2.离线用户的使用姿势(可选阅读)
考虑到内网用户无法联网进行arthas初始化,所以arthas也人性化的提供了全量包的下载方式,有需要的读者可移步到下载页面选择全量包下载即可获取全量的arthas包:
完成后下载并解压之后,我们可以直接通过下面这个脚本指令快速启动arthas:
./as.sh
3.常见指令介绍
步入arthas我们就可以进行一些比较基础的操作,以下是笔者日常用的比较多的指令,和Linux差不多,读者可自行参阅了解
- cat:打印文件内容。
- cls:清空当前屏幕区域内容。
- grep:匹配查找。
- history:打印历史命令。
- pwd:输入当前Java进程所在的位置。
- quit:退出当前arthas客户端。
- stop:关闭arthas服务端,所有arthas客户端都会退出。
这里笔者就简单的演示一下,可以看到pwd输出的就是笔者所监控的Java进程所处的文件目录:
[arthas@9121]$ pwd
# 当前监控的进程在服务器上的目录
/home/sharkchili/arthasExample
[arthas@9121]$
又比如笔者通过memory查看堆内存使用情况,如果只想看老年代的数据,就可以使用grep:
memory |grep ps_old_gen
点击quit会直接退出当前进程的客户端,stop同理只不过多是连着服务端和其他客户端一并杀掉,这里就不多做演示了:
[arthas@9121]$ quit
# 直接返回到服务器的目录
sharkchili@DESKTOP-7IPKPVJ:~/arthas$
4.快捷启动配置
为了快捷启动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"
然后通过source指令使其生效:
可以看到完成这样一段配直接后,我们可以直接通过as指令完成arthas的快捷启动:
# 键入as
sharkchili@DESKTOP-xxx:~$ as
# 直接快速启动arthas
[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
三、Arthas中比较常用的运维指令
1.查看实时数据面板
日常开发维护过程中对于项目的巡检还是蛮重要的,通过Arthas的dashboard可以非常直观的查看当前系统中进程的运行情况。
在arthas的控制面板输入dashboard,默认情况下5s进行一次刷新:
这里我们来简单介绍一下第一板块线程中的字段的含义:
- ID:java级别的线程id号,注意与jstack中的native id的区别。
- NAME:线程名称。
- GROUP:线程所在线程组名。
- PRIORITY:线程优先级,值越大优先级越高。
- STATE:线程运行状态。
- CPU%:线程CPU使用率。
- DELTA_TIME:上次采样之后,线程运行的CPU时间,单位为秒。
- TIME:线程运行的总CPU时长,数据格式为分:秒。
- INTERRUPTED:线程当前的中断位状态。
- DAEMON :是否为守护线程。
第二板块就是内存使用版块,记录各个堆区、元空间的内存使用情况以及GC情况。而第三板块则是服务器运行参数版块,这一版块记录着程序当前运行服务器的内核版本信息、jdk版本等。
需要了解的是arthas中的操作指令可以通过--help了解查阅,我们以dashboard为例,其使用说明如下,可以看到我们可以通过-i决定面板刷新间隔(单位是毫秒),用-n决定面板刷新次数:
所以如果我们希望每1s刷新1次,刷新5次,那么对应的命令就是:
dashboard -i 1000 -n 5
2.查看JVM信息
arthas也可能非常直观的查看jvm信息,对应的指令也就是jvm。
同样的这个指令也会输出多个板块的内容,我们先来看看第一个板块,可以可以看到该指令可以非常直观的看到机器名称、jvm启动时间、jdk版本以及我们配置jvm参数信息:
由于板块比较多,这里笔者就说几个笔者比较常用的板块,分别是线程板块和文件描述符板块,通过这两个板块笔者可以日常巡检了解是否发生线程死锁或者程序中是否出现资源未能及时关闭的情况:
- THREAD:它记录当前活跃线程数、活跃的守护线程数、从JVM启动开启曾经活着的最大线程数、总共启动线程数以及发生死锁的线程数。
- FILE-DESCRIPTOR:这个板块记录JVM可以打开的最大文件描述符和当前已经打开的文件描述符数。
3.查看和修改日志
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";
}
对应的输出结果为:
2024-08-19 23:53:29.454 INFO c.e.a.TestController :138 http-nio-8080-exec-1 info logger
接下来我们就直接通过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
4.查看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指令中各个字段的含义:
5.查看JVM的环境变量
arthas的sysenv指令常用于获取系统环境变量信息,键入这条指令我们可以看到当前java程序所使用的系统大部分环境变量信息,如下图,可以看到大部分的当前系统用户名称、编码格式、当前程序路径以及客户端ip和端口号等信息:
根据help的提示,这条指令同样也支持查询单个环境变量,不过意义不大,毕竟不是每个都知道环境变量叫什么,只有查看了才知道(笑):
[arthas@23543]$ sysenv PWD
KEY VALUE
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
PWD /home/sharkchili/arthasExample
6.查看和修改JVM系统属性
sysprop查看的jvm的系统属性,基本上通过这个指令我们可以看到大部分的JVM参数配置信息,如下输出结果,我们大体可以看到JDK版本、程序名称以及日志编码格式和当前系统用户名称等:
7.查看当前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();
}
此时,键入-b指令就可以定位阻塞其他线程的线程以及所在代码段:
8.vmtool对于JVM的调控
vmtool算是笔者用的比较多的一个工具指令,可用于查询对象或者强制GC等功能,这些功能读者可自行参考官网查阅:
vmtool:https://arthas.aliyun.com/doc/vmtool.html
而笔者这里想要介绍的是一个强制打断线程的功能,这个指令对于特定场景下应急处理还是蛮实用的。
我们的程序调用下面的接口被系统监控到CPU100%,此时我们就可以通过arthas进行特定场景下的应急处理:
@RequestMapping("cpu-100")
public static void cpu() {
//无限循环输出打印
new Thread(() -> {
while (true) {
log.info("cpu working");
}
}, "thread-1").start();
}
我们通过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";
}
如果我们希望查看此刻dateTimeStr 的值,我们就可以通过vmtool的action指定为getInstances ,然后指定类的全路径(以笔者这段代码为例则是com.example.arthasExample.TestController),最后键入表达式instances[0].dateTimeStr意为获取当前实例的dateTimeStr:
vmtool --action getInstances --className com.example.arthasExample.TestController --express 'instances[0].dateTimeStr'
此时我们就可以非常直观的监控到这个变量的信息了:
常见面试题:arthas统计方法耗时的原理是什么
watch指令示例如下:
watch com.example.MyService sayHello "{params, result, throwExp} -> { return 'Exception: ' + throwExp; }" -E
我们从指令即可看出他可以获取到类的全路径,对此我们不拿猜测它就是基于这个类的全路径进行一个字节码桩技术对类进行增强,插入一段代码进行在方法执行前后插入时间,实现耗时统计。