Arthas入门手册
一份简单的Arthas入门手册
介绍
Arthas是一款开源的Java诊断工具
Arthas拥有实时反编译, 修改内存, 查询类加载器, 监控特定类或方法等功能, 能有效检查并解决生产环境中的各类问题
本文将介绍工具的下载运行与一些常用的指令使用方法, 更多详细内容还行查看官方文档: Arthas 用户文档
本文环境
系统: CentOS 7 虚拟机
JDK: OracleJDK1.8.0
Arthas: 3.5
下载 & 运行
下载
Arthas下载地址: https://arthas.aliyun.com/arthas-boot.jar
wget:bashwget https://arthas.aliyun.com/arthas-boot.jar运行
Arthasbashjava -jar arthas-boot.jar若直接运行如上命令, 可能产生如下第二行信息, 这只是说明当前没有其他正在运行的的
Java进程, 没有可诊断对象但是若有其他
Java程序正在运行, 却依然产生此信息, 则说明JDK安装版本不满足要求. 可能安装的是OpenJDK, 需要重新安装OracleJDK# java -jar arthas-boot.jar [INFO] arthas-boot version: 3.5.3 [INFO] Can not find java process. Try to run `jps` command lists the instrumented Java HotSpot VMs on the target system. Please select an available pid.(可选)下载运行测试程序
官方提供了一个用于测试的
SpringBoot程序, 可以运行此程序后再运行Arthaswget:bashwget https://github.com/hengyunabc/spring-boot-inside/raw/master/demo-arthas-spring-boot/demo-arthas-spring-boot.jar运行:
bashjava -jar demo-arthas-spring-boot.jar也可以运行其他的
Java程序用于诊断, 下文的一些诊断内容基于此程序重新运行
Arthas重新运行
arthas-boot.jar后产生类似如下内容:# java -jar arthas-boot.jar [INFO] arthas-boot version: 3.5.3 [INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER. * [1]: 1312 demo-arthas-spring-boot.jar选择进程
输入
1并回车, 此操作是选择运行中demo-arthas-spring-boot.jar进程作为诊断对象, 产生如下内容:# java -jar arthas-boot.jar [INFO] arthas-boot version: 3.5.3 [INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER. * [1]: 2135 demo-arthas-spring-boot.jar 1 [INFO] arthas home: /root/.arthas/lib/3.5.3/arthas [INFO] Try to attach process 2135 [INFO] Attach process 2135 success. [INFO] arthas-client connect 127.0.0.1 3658 ,---. ,------. ,--------.,--. ,--. ,---. ,---. / O \ | .--. ''--. .--'| '--' | / O \ ' .-' | .-. || '--'.' | | | .--. || .-. |`. `-. | | | || |\ \ | | | | | || | | |.-' | `--' `--'`--' '--' `--' `--' `--'`--' `--'`-----' wiki https://arthas.aliyun.com/doc tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html version 3.5.3 main_class pid 2135 time 2021-08-26 09:17:45 [arthas@2135]$此时进入了
Arthas工具, 显示ARTHAS的图标, 且输入行首显示为[arthas@xxxx]$
常用命令
退出Arthas exit | quit
quit或exit
$ quit
[root@localhost ~]#
$ exit
[root@localhost ~]#如此方式退出时, 并没有关闭Arthas工具的进程
重新连接Arthas只需重新输入运行命令
关闭Arthas stop
$ stop
Resetting all enhanced classes ...
Affect(class count: 0 , method count: 0) cost in 1 ms, listenerId: 0
Arthas Server is going to shutdown...
$ session (a0783afa-2aa7-4d61-8624-c978b261e2ab) is closed because server is going to shutdown.
[root@localhost ~]#查看指令帮助
查看指令与介绍help
显示内容较多, 此处截取部分展示
$ help
NAME DESCRIPTION
help Display Arthas Help
auth Authenticates the current session
...查看单条指令详细信息-h
每条指令也都可添加参数-h来获取详细信息, 以dashboard指令为例:
$ dashboard -h
USAGE:
dashboard [-h] [-i <value>] [-n <value>]
SUMMARY:
Overview of target jvm's thread, memory, gc, vm, tomcat info.
EXAMPLES:
dashboard
dashboard -n 10
dashboard -i 2000
WIKI:
https://arthas.aliyun.com/doc/dashboard
OPTIONS:
-h, --help this help
-i, --interval <value> The interval (in ms) between two executions, default is 5000 ms.
-n, --number-of-execution <value> The number of times this command will be executed.查看系统实时数据面板dashboard
dashboard会循环显示系统实时数据
可以通过按键Q或Ctrl + C退出循环
显示内容较多, 此处截取部分展示
$ dashboard
ID NAME GROUP PRIORITY STATE %CPU DELTA_TI TIME INTERRUP DAEMON
-1 C2 CompilerThread0 - -1 - 0.0 0.000 0:3.727 false true
-1 VM Periodic Task Thread - -1 - 0.0 0.000 0:2.710 false tru
...
Memory used total max usage GC
heap 43M 79M 483M 9.02% gc.copy.count 79
eden_space 10M 22M 133M 7.96% gc.copy.time(ms) 287
...
Runtime
os.name Linux
os.version 3.10.0-1160.el7.x86_64
...查看线程信息thread
thread会显示当前的线程信息, 可以检查线程状态, CPU使用率等
显示内容较多, 此处截取部分展示
$ thread
Threads Total: 35, NEW: 0, RUNNABLE: 11, BLOCKED: 0, WAITING: 14, TIMED_WAITING: 5, TERMINATED: 0, Internal
threads: 5
ID NAME GROUP PRIORITY STATE %CPU DELTA_TI TIME INTERRUP DAEMON
-1 C1 CompilerThread1 - -1 - 0.48 0.000 0:1.490 false true
44 arthas-command-execute system 5 RUNNABLE 0.21 0.000 0:0.055 false true
...可以通过参数-b来查看阻塞的线程, 参数-n x来查看最占用cpu的前x个线程
查看JVM已加载的类信息sc
查看JVM中UserController类的信息
$ sc com.example.demo.arthas.user.UserController -d
class-info com.example.demo.arthas.user.UserController
code-source file:/root/demo-arthas-spring-boot.jar!/BOOT-INF/classes!/
name com.example.demo.arthas.user.UserController
...查看已加载类的方法信息sm
查看JVM中UserController中的方法信息
$ sm com.example.demo.arthas.user.UserController -d
declaring-class com.example.demo.arthas.user.UserController
constructor-name <init>
modifier public
...或者指定查看的方法为findUserById
$ sm com.example.demo.arthas.user.UserController findUserById -d
declaring-class com.example.demo.arthas.user.UserController
method-name findUserById
modifier public
..反编译jad
反编译UserController类
$ jad com.example.demo.arthas.user.UserController
ClassLoader:
+-org.springframework.boot.loader.LaunchedURLClassLoader@5674cd4d
+-sun.misc.Launcher$AppClassLoader@70dea4e
+-sun.misc.Launcher$ExtClassLoader@52fb0a15
Location:
file:/root/demo-arthas-spring-boot.jar!/BOOT-INF/classes!/
/*
* Decompiled with CFR.
*
* Could not load the following classes:
* org.slf4j.Logger
* org.slf4j.LoggerFactory
* org.springframework.web.bind.annotation.GetMapping
* org.springframework.web.bind.annotation.PathVariable
* org.springframework.web.bind.annotation.RestController
*/
package com.example.demo.arthas.user;
import com.example.demo.arthas.user.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@GetMapping(value={"/user/{id}"})
public User findUserById(@PathVariable Integer id) {
/*15*/ logger.info("id: {}", (Object)id);
/*17*/ if (id != null && id < 1) {
throw new IllegalArgumentException("id < 1");
}
return new User(id, "name" + id);
}
}
Affect(row-cnt:1) cost in 183 ms.添加参数--source-only可以隐藏ClassLoader与Location信息
可以使用>来输出到文件中
$ jad com.example.demo.arthas.user.UserController > /tmp/UserController.java也可以反编译特定的函数
$ jad com.example.demo.arthas.user.UserController findUserById --source-only --lineNumber false
@GetMapping(value={"/user/{id}"})
public User findUserById(@PathVariable Integer id) {
logger.info("id: {}", (Object)id);
if (id != null && id < 1) {
throw new IllegalArgumentException("id < 1");
}
return new User(id, "name" + id);
}监控函数的返回值watch
监控UserController的findUerById方法的返回值
可以通过按键Q或Ctrl + C退出监控
$ watch com.example.demo.arthas.user.UserController findUserById -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 47 ms, listenerId: 2
method=com.example.demo.arthas.user.UserController.findUserById location=AtExit
ts=2021-09-08 16:36:13; [cost=0.289593ms] result=@ArrayList[
@Object[][
@Integer[1],
],
@UserController[
logger=@Logger[Logger[com.example.demo.arthas.user.UserController]],
],
@User[
id=@Integer[1],
name=@String[name1],
],
]这里使用参数-x指定了展开层数为2, 所以才能看到返回的User的具体内容
可以指定参数-e, 只在发生异常时进行显示
监控节点耗时trace
监控UserController的findUerById方法调用耗时, 每个方法的耗时显示在行最前端
$ trace com.example.demo.arthas.user.UserController findUserById
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 65 ms, listenerId: 4
`---ts=2021-09-08 17:19:26;thread_name=http-nio-80-exec-5;id=14;is_daemon=true;priority=5;TCCL=org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedWebappClassLoader@11858c60
`---[0.269505ms] com.example.demo.arthas.user.UserController:findUserById()
+---[0.198111ms] org.slf4j.Logger:info() #15
`---[0.007955ms] com.example.demo.arthas.user.User:<init>() #21可以使用参数--skipJDKMethod false来监控JDK函数耗时
使用案例
热更新代码
目前, 请求http://localhost/user/0会产生java.lang.IllegalArgumentException错误
# curl http://localhost/user/0
{"timestamp":1631170739465,"status":500,"error":"Internal Server Error","exception":"java.lang.IllegalArgumentException","message":"id < 1","path":"/user/0"}原因在UserController.findByUserId函数内, 现在通过热更新代码来解决这一错误
通过
jad命令将UserController类反编译为java文件, 并保存至本地$ jad --source-only com.example.demo.arthas.user.UserController --lineNumber false > /tmp/UserController.java使用
vi或vim编辑文件, 编辑后内容如下javapackage com.example.demo.arthas.user; import com.example.demo.arthas.user.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { private static final Logger logger = LoggerFactory.getLogger(UserController.class); @GetMapping(value={"/user/{id}"}) public User findUserById(@PathVariable Integer id) { logger.info("id: {}", (Object)id); if (id != null && id < 1) { return new User(id, "name" + id); // throw new IllegalArgumentException("id < 1"); } return new User(id, "name" + id); } }通过
sc命令查找到UserController类的类加载器的hash值$ sc -d *UserController | grep classLoaderHash classLoaderHash 5674cd4d*为通配符, 类似模糊查找grep为结果筛选, 此处只需要classLoaderHash的值, 本次的值为5674cd4d通过
mc命令将UserController.java文件编译$ mc -c 5674cd4d /tmp/UserController.java -d /tmp Memory compiler output: /tmp/com/example/demo/arthas/user/UserController.class Affect(row-cnt:1) cost in 466 ms.此处需要指定类的加载器, 已经通过第四步获取到
-d参数指定编译文件的输出路径, 也存放在/tmp中通过
redefine命令重新加载编译后的文件$ redefine /tmp/com/example/demo/arthas/user/UserController.class redefine success, size: 1, classes: com.example.demo.arthas.user.UserController
此时请求http://localhost/user/0则正常返回数据
# curl http://localhost/user/0
{"id":0,"name":"name0"}线程死锁诊断
上述例子中不存在线程死锁的情况, 故使用以下例子来进行测试
package io.tomoto.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 线程死锁测试Controller
* </p>
*
* @author Tomoto
* @version 1.0
* @since 1.0 2021/9/9 16:34
*/
@RestController
public class TestController {
private Object key1 = new Object();
private Object key2 = new Object();
@GetMapping("/deadlock")
public void deadlock() {
System.out.println("start deadlock");
new Thread(() -> {
synchronized (key1) {
try {
Thread.sleep(2000);
} catch (InterruptedException ignored) {
}
synchronized (key2) {
System.out.println("Thread1 key2");
}
}
}).start();
new Thread(() -> {
synchronized (key2) {
try {
Thread.sleep(2000);
} catch (InterruptedException ignored) {
}
synchronized (key1) {
System.out.println("Thread2 key1");
}
}
}).start();
}
}在请求该地址之前, 使用thread -b命令, 提示没有阻塞的线程
$ thread -b
No most blocking thread found!在请求后, 再次使用thread -b, 提示存在阻塞的线程, 并指出代码位置
$ thread -b
"Thread-4" Id=41 BLOCKED on java.lang.Object@30cdfdb0 owned by "Thread-5" Id=42
at io.tomoto.controller.TestController.lambda$deadlock$0(TestController.java:32)
- blocked on java.lang.Object@30cdfdb0
- locked java.lang.Object@16271c06 <---- but blocks 1 other threads!
at io.tomoto.controller.TestController$$Lambda$536/391834385.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)之后就可以修改代码重新打包部署或者通过热更新代码来解决了
诊断Docker容器运行的Java程序
进入容器, 安装运行Arthas
docker exec -it ${containerId} /bin/bash -c "wget https://arthas.aliyun.com/arthas-boot.jar && java -jar arthas-boot.jar"可能遇到的问题
wget失败原因: 无法
wget到arthas-boot.jar文件, 错误信息为wget: can't execute 'ssl_helper': No such file or directory
则是因为容器内置的wget不支持https解决方案: 需要从其他的
http地址获取该文件, 或者在构建镜像时在Dockerfile中添加该文件dockerfileADD arthas-boot.jar arthas-boot.jar
无法启动
Arthas原因: 若无法运行
Arthas, 可能是因为容器内安装的是JRE而非JDK, 或安装的是低版本的OpenJDK, 缺少必要工具, 上文也有提到过此原因解决方案: 在
Dockerfile中添加如下内容并重新构建镜像dockerfileFROM java:8-alpine
Arthas启动失败原因: 镜像启动时若直接运行
Java进程,pid为1, 会导致一些功能无法使用, 这是docker自身的问题解决方案: 若
docker版本大于等于1.13, 则可以在启动镜像时添加参数--init来避免Java进程pid为1bashdocker run --init -dp 80:80 demo