上一篇的文章讲到了Arthas的安装启动和JVM 相关命令。这篇文章我们继续把类加载相关命令,方法监控和跟踪相关命令讲完。
类加载相关命令
现版本的类加载命令包括:
- 1 sc命令 -> 查看JVM已经加载的类。
- 2 sm命令 -> 查看已经加载的类的方法。
- 3 dump命令 -> dump已加载类的bytecode到特定目录
- 4 redefine命令 -> 加载外部的.class文件,并redefine到JVM里面
- 5 jad命令 -> 反编译已经加载类的源码
- 6 classloader命令 -> 查看classloader 的继承数,urls,类加载信息等。
sc命令
search-class的简写,可以根据类名和方法查找已经加载的类.
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
[d] | 输出当前类的详细信息 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
[f] | 输出当前类的成员变量信息 |
[x:] | 指定输出静态变量是属性的遍历深度。默认为0 |
额外说明: 1)class-pattern 支持类的全限定名,支持com.x.x.x的形式,也支持从log直接拷贝过来com/x/x/x的形式。2)sc命令默认开启了子类匹配功能,也就是说子类也可以被搜索出来,如果想要关闭此参数,利用命令 option disable-sub-class false
使用示例:
我们修改一下StaticDemo 类,并编译和启动:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class StaticDemo {
public static Map<String, String> map = new HashMap<String, String>(){{put("111","111");}};
private Integer value1 = 1;
private static Integer value2 = 2;
public static void main(String[] args){
ArrayList list = new ArrayList();
list.add("aaa");
//阻塞main方法
Object lock = new Object();
try {
synchronized (lock){
while (true){
lock.wait();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("进程退出");
}
}
我们同样进入启动好的StaticDemo 进程,我们输入sc -df java.util.ArrayList,查看ArrayList类的详细信息和成员变量信息:
我们可以看到类全名,是否为接口,是否为注解,是否为枚举类型,是什么访问权限,有哪一些属性,加载源头和使用的加载器是什么等等这些都可以一览无余,可以说是非常方便。
sm命令
search-method 的缩写。能够搜索已经加载的类的所有方法信息。sm 命令只能看到当前类的声明方法,并不能看到父类的声明方法。
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
method-pattern | 方法名表达式匹配 |
[d] | 输出当前类的详细信息 |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
使用示例:
输出sm java.util.ArrayList, 查看ArrayList 所有的方法
输入sm -d java.util.ArrayList add查看add 方法的详细信息:
dump命令
dump 命令可以将已经加载类的字节码放到特定目录。
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
[c:] | 类所属ClassLoader 的 hashCode |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
使用示例:
进入StaticDemo 进程输入dump -E java.util.HashMap
如图所示,第一列信息是类所属ClassLoader 的hashCode,如果想单单输出一个类的字节吗,也可以用 -c指定。
第二列是加载器信息,第三列是字节码存储的地方。
jad命令
jad 命令可以将JVM 时机运行的字节码反编译出来。反编译出来的源码可能字符集和语法可能存在错误,但是不影响阅读理解。
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式匹配 |
[c:] | 类所属ClassLoader 的 hashCode |
[E] | 开启正则表达式匹配,默认为通配符匹配 |
使用示例:
进入StaticDemo 进程输入jad StaticDemo
我们发现其实对比源码,还是有一些区别的,不过不影响阅读理解。
如果当加载的类过多的时候,我们还以利用-c 参数来指定哪个类的源码。官网还可以指定方法,但是我自己试了不行。
classloader 命令
classloader 可以查看JVM中所有的classloader信息统计,并可以展示继承树和urls。
参数说明:
参数名称 | 参数说明 |
---|---|
[l] | 按照类的加载实例进行统计 |
[t] | 打印所有ClassLoader 的继承树 |
[a] | 列出所有Classloader 加载的类。(谨慎使用) |
[c:] | ClassLoader 的hashcode |
[c:r:] | 用ClassLoader 去找resource |
使用示例:
进入StaticDemo 进程,输入classloader

输入classloader -t 查看类加载器的继承树
其他用法大家可以自己去探讨一下。
redefine 命令
加载外部的.class 文件,redefine jvm 已加载的类。(redefine可以直接修改线上的源码,功能非常强大,而且redefine 的类不能恢复,而且redefine有可能失败。所以要小心使用。)
参数说明:
参数名称 | 参数说明 |
---|---|
[c:] | ClassLoader 的hashcode |
[p:] | 外部.class 的完整路径,支持多个路径 |
使用示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//main
public class RedefineDemo {
public static void main(String[] args){
while (true){
JayChowPerson jayChowPerson = JayChowPerson.getInstance();
jayChowPerson.sing();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1 | //bean |
我们将这两个类编译,并启动RedefineDemo程序后。后台日志持续打印
接下来假设因为需求原因,我们需要 原本属性song=qing hua ci 改为 song=yin di an lao ban jiu。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//new bean
public class RedefineDemo {
public class JayChowPerson {
private String name;
private String famousSong;
public static JayChowPerson getInstance(){
return new JayChowPerson("JayChow" , "yin di an lao ban jiu");
}
public JayChowPerson(String name , String famousSong) {
this.name = name;
this.famousSong = famousSong;
}
public void sing(){
System.out.println(name+" sing " + famousSong);
}
}
}
同样使用javac 编译后放在另外一个文件夹中,使用redefine 命令进行替换:
替换后我们查看打印结果立马改变了:
当然我们也可以利用-c 参数指定加载器,这里就不在操作了。
监控跟踪命令相关
现版本的监控和跟踪的命令包括:
- 1 monitor方法 -> 方法执行监控
- 2 watch方法 ->方法执行数据监测
- 3 trace -> 方法内部调用路径,并输出方法路径上的每个节点上的耗时。
- 4 stack -> 输出当前方法被调用的调用路径。
- 5 tt -> 方法执行数据的时空隧道。
需要注意的是这些方法都是通过字节码增强技术实现的,在线上使用时,请明确需要观测的类,方法,以及条件。使用完成之后需要执行shutdown 或者 将增强过的类执行reset 命令。
我们重新写一下demo: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
43public class CalculationDemo {
public static void main(String[] args){
while (true){
List<Integer> arr1 = new ArrayList<Integer>();
arr1.add(1);
arr1.add(9);
execute(arr1);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void execute(List<Integer> arr1){
int basicValue = 0;
for (int i = 0; i < arr1.size(); i++){
basicValue += arr1.get(i);
}
int extendValue = 10;
beforeAdd();
add(basicValue, extendValue);
afterAdd();
}
private static int add(Integer value1 , Integer value2){
System.out.println("adding");
return new BigDecimal(value1).add(new BigDecimal(value2)).intValue();
}
private static void beforeAdd(){
System.out.println("before add");
}
private static void afterAdd(){
System.out.println("after add");
}
}
monitor 命令
monitor 命令非实时命令,直白一点来说就是当方法被调用的时候,才会监控到方法的一些信息。
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式 |
method-pattern | 方法表达式 |
[E] | 开启正则表达式 |
[c:] | 统计周期,默认为120秒(默认要等两分钟才打印,第一次以为没有打印) |
使用示例:
我们输入 monitor -c 10 CalculationDemo execute
我们可以从监控屏看到方法在周期时间内的调用总次数,成功次数,失败次数,平均花费的时间和失败率。
watch 命令
watch 可以方便的查看方法的参数,返回值等信息。
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式 |
method-pattern | 方法表达式 |
express | 观察表达式 |
condition-express | 条件表达式 |
[b] | 在方法开始前观察 |
[e] | 在方法异常后观察 |
[s] | 在方法返回后观察 |
[f] | 在方法结束后观察(默认) |
[E] | 开启正则表达式 |
[x:] | 对输出结果进行遍历的深度,默认为1 |
特别说明: express 只有是符合ognl 表达式的基本都能被正常支持,我们可以使用 “{params, returnObj}” 来观察方法的参数和返回结果。 这里要注意的是params 在使用 -b 参数的时候,观察到的值是方法入参,此时异常和返回值均不存在,而 -e -s -f 观察到的值是方法出参。
使用示例:
我们同样进入CalculationDemo 进程,输入 watch CalculationDemo add “{params, returnObj}” -b -x 2
我们可以看见方法入参为两个Integer值,返回值为null (因为参数为-b)
输入 watch CalculationDemo add “{params, returnObj}” -s -x 2,我们可以观察方法出参和返回值
那我们怎么使用条件表达式呢?我们输入 watch CalculationDemo add “{params, returnObj}” -x 2 -s params[0]==10
这个条件表达式的意思是,只有当第一个参数的值满足等于10的时候,此命令才会有响应。
如果我们在此程序将条件修改为 params[0] == 2,我们会发现由于条件不成立,则会没有打印。
有时候我们需要过滤导致异常的参数信息,我们可以利用 -e 参数: watch CalculationDemo verify “{params, throwExp}” -e -x 2
trace 命令
trace 命令可以统计整个调用链路的性能开销和链路追踪。trace 在执行的过程中自身会有一定的开销,这个统计有点类似JProfile的功能,但是JProfile 会减去自身执行的开销时间,相对来说会比较准确,而且方法越多,trace 可能偏差越大。虽然有偏差,但是还是能看出不少问题的。
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式 |
method-pattern | 方法表达式 |
condition-express | 条件表达式 |
[E] | 开启正则表达式 |
[n:] | 命令执行次数 |
#cost | 方法执行耗时(单位毫秒) |
使用示例:
进入CalculationDemo 进程后,输入trace CalculationDemo execute
[3.8..ms]为execute 方法的执行时间
[min=0.008697ms,max=0.652564ms,total=0.661261ms,count=2] 对相同方法的调用进行了合并。
[throws Exception]表面这个方法有抛出异常。
我们也可以像watch 命令一样利用condition-express(条件表达式) 和 #cost(消耗时间) 进行过滤,用法跟watch一样。
我们这里利用消耗时间来过滤: trace CalculationDemo execute #cost>0.7
stack 命令
很多时候我们知道一个方法被执行了,但是调用这个方法的执行路径特别多,导致根本不知道是从哪里被调用了。这时候stack命令就显得特别有用。
写一个简单的例子: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
34public class StackDemo {
public static void main(String[] args){
while (true){
for (int i = 1; i <= 10; i++){
if(i % 2 == 0){
evenNumber(i);
}else {
oddNumber(i);
}
}
}
}
public static void oddNumber(int number){
System.out.println("We accept odd number:"+number);
//do something
sleepByNumber(number);
}
public static void evenNumber(int number){
System.out.println("We accept evenNumber:"+number);
//do something
sleepByNumber(number);
}
private static void sleepByNumber(int number){
try {
Thread.sleep(number*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式 |
method-pattern | 方法表达式 |
condition-express | 条件表达式 |
[E] | 开启正则表达式 |
[n:] | 命令执行次数 |
[s:] | 检索参数 |
[i] | 查看调用信息 |
使用示例:
同样编译和运行StackDemo,进入进程后,我们输入命令:stack StackDemo sleepByNumber ‘params[0]==3
我们打印打印number为单数,且执行时间大于5的执行链路
tt命令
tt 命令是TimeTunnel 的缩写。可以记录指定方法每次的调用入参和返回信息。并且能对这些调用进行对比和观察。
我们还是进入StackDemo, 观察sleepByNumber 的调用
参数说明:
参数名称 | 参数说明 |
---|---|
class-pattern | 类名表达式 |
method-pattern | 方法表达式 |
condition-express | 条件表达式 |
[t] | 主参数,表面希望记录下类的每次执行情况。 |
[n] | 表示记录的次数 |
[p] | 重新执行调用 |
使用示例:
进入StackDemo 输入: tt -t -n 30 StackDemo sleepByNumber
字段说明:
字段名称 | 字段说明 |
---|---|
INDEX | 索引号,每一次调用对应着一个索引。如果想查看针对某一次调用的信息必须基于此索引编号 |
TIMESTAMP | 方法执行的时候,本地的时间 |
COST | 方法执行的耗时 |
IS-RET | 方法是否以正常返回的形式结束 |
IS-EXP | 方法是否以抛异常的形式结束 |
OBJECT | 执行对象的hashCode ,但跟JVM 的hashcode 并不是同一个东西 |
CLASS | 执行的类名 |
METHOD | 执行的方法名 |
我们用上面的tt命令记录一大片时间段之后,可以利用条件从中筛选出自己想要看到的记录。
首先我们输入命令 tt -l,查看所有记录
然后我们利用 -s 参数进行检索:tt -s params[0]==5
我们也可以利用-i参数对某一次调用进行查看:tt -i 1001,这个1001就是记录信息里面的INDEX的值。
重做一次调用:有时候我们想要重现其中一次调用,我们可以直接用tt命令 加上-p 参数即可,再也不用前端人员点击按钮出发多一次啦。
1.我们输入 tt -t StaticDemo oddNumber -n 3 记录三次调用
2.然后我们使用 tt -i 1031 -p 触发再一次执行方法。
总结
这篇文章主要介绍了Arthas 类加载相关命令,以及方法监控与跟踪相关命令。特别是方法监控与跟踪在解决线上问题起到非常好的作用,但是有些命令过于强大也要谨慎使用,例如可以让JVM 动态重新加载class 文件的redefine 命令。至此我们对Arthas 3.0版本的命令都有了大致的了解,这的确是一款很不错的线上诊断工具,希望下个版本能解决现有的小不足,增加一些更让我们惊叹的功能。