arthas学习图文记录
Arthas 是阿里开源的 Java 诊断工具。在线排查问题,无需重启;动态跟踪 Java 代码;实时监控 JVM 状态。Arthas 支持 JDK 6+,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断。
Arthas可以通过简单的命令交互模式,接入运行的JVM,快速定位和诊断线上程序运行的问题。在不重启服务的情况下,实时,动态的修改相关代码,并实时生效,具体工作原理如下:
- 连接JVM,通过attach机制,通过attach pid连接正在运行的JVM
- 查看及修改JVM字节码,通过instrument技术对运行中的JVM附加或修改字节码来实现增强的逻辑
Arthas的执行过程如下:Arthas底层调用rt.jar包的ManagementFactory获取整个JVM内部信息,通过命令集成与后端交互,执行,返回结果,整个工程简单清晰,容易上手。
arthas-demo入门
可以使用阿里云给的基础教程地址练习: https://arthas.aliyun.com/doc/arthas-tutorials.html?language=cn&id=arthas-basics
在这里,我使用自己的服务器,跟着基础教程做入门联系。
1.下载math-game.jar
,再用java -jar
命令启动
[root@localhost arthas]# wget https://arthas.aliyun.com/math-game.jar
[root@localhost arthas]# java -jar arthas-boot.jar
2.新开Terminal
,下载arthas-boot.jar
,再用java -jar
命令启动
[root@localhost arthas]# wget https://arthas.aliyun.com/arthas-boot.jar
[root@localhost arthas]# java -jar arthas-boot.jar
arthas-boot
是Arthas
的启动程序,它启动后,会列出所有的Java进程,用户可以选择需要诊断的目标进程。
选择第一个进程,输入 1
(math-game这个程序对应的就是1),再Enter/回车
:
Attach成功之后,会打印Arthas LOGO。输入 help
可以获取到更多的帮助信息。
3. dashboard
命令可以查看当前系统的实时数据面板
数据说明:
- ID:java级别的线程ID,注意这个ID不能跟jstack中的nativeID一一对应
- NAME:线程名
- GROUP:线程组名
- PRIORI:线程的优先级,1~10之间的数字,越大表示优先级越高
- STATE:线程的状态
- %CPU:线程的CPU使用率,比如采样间隔1000ms,某个线程的增量cpu时间为100ms,则cpu的使用率为100/1000=10%
- DELTA_TIME:上次采样之后线程运行增量cpu时间,数据格式为秒
- TIME:线程运行总CPU时间,数据格式为 分:秒
- INTERRUPTED:线程当前的中断位状态
- DAEMON:是否是daemon(后台)线程
4. thread
命令会打印线程ID 1的栈
还可以通过thread 1 | grep 'main('
命令来查找main class:
参数说明:
参数名称 | 参数说明 |
---|---|
id | 线程id |
[ n: ] | 指定最忙的前N个线程并打印堆栈 |
[ b ] | 找出当前阻塞其他线程的线程(目前只支持找出synchronized关键字阻塞住的线程, 如果是java.util.concurrent.Lock , 目前还不支持。) |
[ i ] | 指定cpu占比统计的采样间隔,单位为毫秒 |
[ --all ] | 显示所有匹配的线程 |
[ --state ] | 查看指定状态的线程,如: thread --state WAITING |
5. sc
命令来查找JVM里已加载的类
sc为“Search-Class” 的简写,能搜索出所有已经加载到 JVM 中的 Class 信息。
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配,支持全限定名。如:demo.MathGame,也支持demo/MathGame |
method-pattern | 方法名表达式匹配 |
[ d ] | 输出当前类的详细信息,包括这个类所加载的原始文件来源,类的声明,加载的ClassLoader等详细信息;如果一个类被多个ClassLoader所加载,则会出现多次 |
[ E ] | 开启正则表达式匹配,默认为通配符匹配 |
[ f ] | 输出当前类的成员变量信息(需要配合参数 -d一起使用) |
[ x: ] | 指定输出静态变量时属性的遍历深度,默认为0,直接使用toString输出 |
[ c: ] | 指定class的ClassLoader的hashcode |
[ classLoaderClass: ] | 指定执行表达式的ClassLoader的class name |
[ n: ] | 具有详细信息的匹配类的最大数量,默认为100 |
sm为“Search-Method” 的简写,查询某个类下所有的方法,与sc的功能类似,这里就不详细介绍了。
6. jad
命令来反编译代码
还可以反编译指定的函数
反编译时只显示源代码
默认情况下,反编译结果里会带有ClassLoader
信息,通过--source-only
选项,可以只打印源代码。这样就会清爽很多。
7. watch
方法执行数据观测
watch命令可以查看函数的参数/返回值/异常信息,通过编写 OGNL 表达式进行对应变量的查看。
从上面的结果里,说明函数被执行了两次,第一次结果是 location=AtExceptionExit,说明函数抛出了异常,因此returnObj是null;第二次结果是location=AtExit,说明函数正常返回,因此可以看到returnObj的结果是一个ArrayList。
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
express | 观察表达式,默认值为:{params,target,returnObj},单个值可以不用加 {},多个值需要加 |
condition-express | 条件表达式,不能加 {},可以使用逗号分隔子表达式,取表达式最后一个值来判断 |
[ b ] | 在方法调用之前观察,默认关闭,由于观察事件点是在方法调用前,此时返回值或异常均不存在,params代表方法入参 |
[ e ] | 在方法异常之后观察,默认关闭,params代表方法出参 |
[ s ] | 在方法返回之后观察,默认关闭,params代表方法出参 |
[ f ] | 在方法结束之后(正常返回和异常返回)观察,默认打开,params代表方法出参 |
[ E ] | 开启正则表达式匹配,默认为通配符匹配 |
[ x: ] | 指定输出结果的属性遍历深度,默认为1 |
[ #cost ] | 监控耗时 |
条件表达式的例子:
下面这个例子表示只有参数小于0的调用才会响应。
异常信息的例子:
-e 表示抛出异常时才触发
express中,表示异常信息的变量时throwExp
8. vmtool
命令,可以搜索内存对象
vmtool
利用JVMTI
接口,实现查询内存对象,强制GC等功能。
参数说明:
参数名称 | 参数说明 |
---|---|
--action getInstances | 返回结果绑定到 instances变量上,它是数组。 |
--className | 指定类名(完成路径),支持 java.lang.String,也支持java/lang/String |
[ --limit ] | 限制返回值数量,避免获取超大数据时对JVM造成压力。默认值是10。 |
[ -x ] | 指定返回结果展开层数,默认为1 |
[ c: ] | 指定class的ClassLoader的hashcode(通过sc命令找到加载class的classLoader) |
[ classLoaderClass: ] | 指定执行表达式的ClassLoader的class name |
强制GC的命令:vmtool --action forceGc
9. 退出Arthas
用 exit
或者 quit
命令可以退出Arthas。退出Arthas之后,还可以再次用 java -jar arthas-boot.jar
来连接。
10. 彻底退出Arthas
exit/quit
命令只是退出当前session,arthas server还在目标进程中运行。
想完全退出Arthas,可以执行 stop
命令。
Arthas的其他重点使用功能
1. mc 内存编译器(Memory Compiler/内存编译器)
Memory Compiler/内存编译器,编译.java
文件生成.class
。通过 -c / --classLoaderClass 参数指定classLoader,-d 参数指定输出目录;编译生成.class文件之后,可以结合retransform
命令实现热更新代码。retransform的限制:1.不允许新增加field和method 2.正在跑的函数,没有退出不能生效
这里还是使用arthas的提供的教程:https://arthas.aliyun.com/doc/arthas-tutorials.html?language=cn&id=command-mc-retransform
1.由于正在跑的函数没有退出时不生效的,所以上面的math-game的demo就不能使用了,所以得下载另一个demo(一个简单的spring-boot应用)。
[root@localhost arthas]# wget https://raw.githubusercontent.com/hengyunabc/spring-boot-inside/master/demo-arthas-spring-boot/demo-arthas-spring-boot.jar
[root@localhost arthas]# java -jar demo-arthas-spring-boot.jar
2.新开一个Terminal
;访问下面这个路径,可以看到报错(500异常)了。
3.启动arthas-boot应用。
4.反编译代码可以看到,当id小于1是,就会抛出异常。
在这里,我们修改文件,想让id小于1时还是能正常返回,不抛出异常。
5.jad反编译UserController,将结果保存在/tmp/UserController.java
文件夹里。
[arthas@1645]$ jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java
6.通过vim /tmp/UserController.java
编辑java类。
7.通过sc查找加载UserContoller的ClassLoader,也可以在jad时显示源码,里面也有classLoader的信息。
以下三个命令任意一个都可以。可以看到,这个java类是由LaunchedURLClassLoader@1be6f5c3
这个类加载器加载的。
8.通过mc命令编译,同时指定--classLoaderClass
参数指定ClassLoader:
[arthas@1645]$ mc --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader /tmp/UserController.java -d /tmp
也可以通过-c
参数指定ClassLoaderHash:
[arthas@1645]$ mc -c 1be6f5c3 /tmp/UserController.java -d /tmp
可以看到,在tmp文件夹下,根据UserController的类的全路径编译了一个class文件:
9.retransform 命令中西加载新编译好的UserContoller.class类:
10.重新编译文件可以看到,代码已经替换成最新的了:
11.访问:
显示已经替换过的类:retransform -l
恢复修改前的代码,清楚指定的类:retransform -d 1
清楚所有的:retransform --deleteAll
2.trace 方法内部调用路径,并输出方法路径上的每个节点上耗时
可以观察方法执行的时候哪个子调用比较慢
[arthas@1645]$ trace ClassName methodName
trace
命令能主动搜索 class-pattern
/method-pattern
对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。在进行性能调优的时候十分有效。
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
express | 观察表达式,默认值为:{params,target,returnObj},单个值可以不用加 {},多个值需要加 |
condition-express | 条件表达式 |
[ E ] | 开启正则表达式匹配,默认为通配符匹配 |
[ n: ] | 命令执行次数 |
[ #cost ] | 方法执行耗时 |
[ --skipJDKMethod ] | 跳过jdk方法,默认为true |
不跳过JDK方法:
只展示耗时大于1ms的调用路径:
动态trace:
从上图中可以看到,primeFactors的方法耗时最长,如果想深入primeFactors方法,可以打开一个新的终端,使用telnet localhost 3658
连接上arthas,在trace primeFactors时指定listenerId。
这时终端2打印的结果,说明已经增强了一个函数:Affect(class count: 1 , method count: 1)
,但不再打印更多的结果。
再查看终端1,可以发现trace的结果增加了一层,打印了primeFactors
函数里的内容:
注意 --listenerId指定的id在前一条命令的输出中可以看到。
3.stack 查看某个函数的调用堆栈路径
很多时候,在一个方法被执行时,方法的执行路径非常多,或者根本就不着调这个方法时从哪里被执行的,就可以使用stack命令。此命令和trace命令结构类似。
[arthas@1645]$ stack demo.MathGame primeFactors
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
express | 观察表达式,默认值为:{params,target,returnObj},单个值可以不用加 {},多个值需要加 |
condition-express | 条件表达式 |
[ E ] | 开启正则表达式匹配,默认为通配符匹配 |
[ n: ] | 命令执行次数 |
4.tt 命令
tt是TimeTunnel 的缩写,tt命令记录方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测。
watch
虽然很方便和灵活,但需要提前想清楚观察表达式的拼写,这对排查问题而言要求太高,因为很多时候我们并不清楚问题出自于何方,只能靠蛛丝马迹进行猜测。
这个时候如果能记录下当时方法调用的所有入参和返回值、抛出的异常会对整个问题的思考与判断非常有帮助。
参数说明:
参数名称 | 参数说明 |
---|---|
-t | 记录下类对应的方法的每次执行情况 |
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
[ n: ] | 命令执行次数 |
condition-express | 条件表达式 |
表格字段说明:
表格字段 | 字段解释 |
---|---|
index | 时间片段记录编号,每一个编号代表着一次调用,后续tt还有很多命令都是基于此编号指定记录操作,非常重要。 |
timestamp | 方法执行的本机时间,记录了这个时间片段所发生的本机时间 |
cost(ms) | 方法执行的耗时 |
is-ret | 方法是否以正常返回的形式结束 |
is-exp | 方法是否以抛异常的形式结束 |
object | 执行对象的hashCode() ,注意,曾经有人误认为是对象在JVM中的内存地址,但很遗憾他不是。但他能帮助你简单的标记当前执行方法的类实体 |
class | 执行的类名 |
method | 执行的方法名 |
检索调用记录:
tt -l
检索所有的调用记录:
筛选出 primeFactors
方法的调用信息:
通过 -i
参数后边跟着对应的 INDEX
编号查看到他的详细信息:
重做一次调用:
tt
命令由于保存了当时调用的所有现场信息,所以我们可以自己主动对一个 INDEX
编号的时间片自主发起一次调用。此时需要使用 -p
参数。通过 --replay-times
指定 调用次数,通过 --replay-interval
指定多次调用间隔(单位ms, 默认1000ms)
需要强调的点
-
ThreadLocal 信息丢失
很多框架偷偷的将一些环境变量信息塞到了发起调用线程的 ThreadLocal 中,由于调用线程发生了变化,这些 ThreadLocal 线程信息无法通过 Arthas 保存,所以这些信息将会丢失。
-
引用的对象
需要强调的是,
tt
命令是将当前环境的对象引用保存起来,但仅仅也只能保存一个引用而已。如果方法内部对入参进行了变更,或者返回的对象经过了后续的处理,那么在tt
查看的时候将无法看到当时最准确的值。这也是为什么watch
命令存在的意义。
5.monitor 方法执行监控
对匹配的class-pattern
/ method-pattern
的类、方法的调用进行监控。monitor
命令是一个非实时返回的命令(并不是输入之后立即返回,而是不断的等待目标java进程返回信息)。
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
[ E ] | 开启正则表达式匹配,默认为通配符匹配 |
[ c: ] | 统计周期,默认值为120秒,是一个整型的参数值 |
监控项说明:
监控项 | timestamp | class | method | total | success | tail | rt | fail-rate |
---|---|---|---|---|---|---|---|---|
说明 | 时间戳 | java类 | 方法 | 调用次数 | 成功次数 | 失败次数 | 平均rt | 失败率 |
5.target-ip
target-ip 为指定绑定的IP,如果不指定IP,Arthas只listen 127.0.0.1,所以如果想从远程连接,则可以使用 --target-ip参数指定listen的IP。
java -jar arthas-boot.jar --target-ip IP
绑定远程访问IP后,可以在通过telnet 或者http的方式远程连接Arthas进行问题排查
6. --telnet-port
如果端口号被占用,也可以通过一下命令绑定另一个端口号来执行
java -jar arthas-boot.jar --telnet-port 9999 --http-port 1
还有更多的功能请查看arthas的官方文档。