Java线上诊断神器Arthas-2

上一篇的文章讲到了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
25
public 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类的详细信息和成员变量信息:
IMAGE
我们可以看到类全名,是否为接口,是否为注解,是否为枚举类型,是什么访问权限,有哪一些属性,加载源头和使用的加载器是什么等等这些都可以一览无余,可以说是非常方便。

sm命令

search-method 的缩写。能够搜索已经加载的类的所有方法信息。sm 命令只能看到当前类的声明方法,并不能看到父类的声明方法。
参数说明:

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 方法名表达式匹配
[d] 输出当前类的详细信息
[E] 开启正则表达式匹配,默认为通配符匹配

使用示例:
输出sm java.util.ArrayList, 查看ArrayList 所有的方法
IMAGE

输入sm -d java.util.ArrayList add查看add 方法的详细信息:
IMAGE

dump命令

dump 命令可以将已经加载类的字节码放到特定目录。

参数说明:

参数名称 参数说明
class-pattern 类名表达式匹配
[c:] 类所属ClassLoader 的 hashCode
[E] 开启正则表达式匹配,默认为通配符匹配

使用示例:
进入StaticDemo 进程输入dump -E java.util.HashMap
IMAGE
如图所示,第一列信息是类所属ClassLoader 的hashCode,如果想单单输出一个类的字节吗,也可以用 -c指定。
第二列是加载器信息,第三列是字节码存储的地方。

jad命令

jad 命令可以将JVM 时机运行的字节码反编译出来。反编译出来的源码可能字符集和语法可能存在错误,但是不影响阅读理解。

参数说明:

参数名称 参数说明
class-pattern 类名表达式匹配
[c:] 类所属ClassLoader 的 hashCode
[E] 开启正则表达式匹配,默认为通配符匹配

使用示例:
进入StaticDemo 进程输入jad StaticDemo
IMAGE
我们发现其实对比源码,还是有一些区别的,不过不影响阅读理解。
如果当加载的类过多的时候,我们还以利用-c 参数来指定哪个类的源码。官网还可以指定方法,但是我自己试了不行。

classloader 命令

classloader 可以查看JVM中所有的classloader信息统计,并可以展示继承树和urls。

参数说明:

参数名称 参数说明
[l] 按照类的加载实例进行统计
[t] 打印所有ClassLoader 的继承树
[a] 列出所有Classloader 加载的类。(谨慎使用)
[c:] ClassLoader 的hashcode
[c:r:] 用ClassLoader 去找resource

使用示例:
进入StaticDemo 进程,输入classloader
![IMAGE](A7DABAEB879663ADF47981334C2FEE04.jpg
第一列为加载器的名称,第二列为加载器实例个数,第三列为加载器的加载类的统计。

输入classloader -l查看实例统计信息
IMAGE
第一列为加载器的名称,第二列为已经加载类的统计,第三列为加载器的hash值,第四列为加载器的父类加载器(双亲委派模型)

输入classloader -t 查看类加载器的继承树
IMAGE

其他用法大家可以自己去探讨一下。

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//bean
public class JayChowPerson {

private String name;

private String famousSong;

public static JayChowPerson getInstance(){
return new JayChowPerson("JayChow" , "qing hua ci");
}

public JayChowPerson(String name , String famousSong) {
this.name = name;
this.famousSong = famousSong;
}

public void sing(){
System.out.println(name+" sing " + famousSong);
}
}

我们将这两个类编译,并启动RedefineDemo程序后。后台日志持续打印
IMAGE

接下来假设因为需求原因,我们需要 原本属性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 命令进行替换:
IMAGE
替换后我们查看打印结果立马改变了:
IMAGE
当然我们也可以利用-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
43
public 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
IMAGE
我们可以从监控屏看到方法在周期时间内的调用总次数,成功次数,失败次数,平均花费的时间和失败率。

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
IMAGE
我们可以看见方法入参为两个Integer值,返回值为null (因为参数为-b)

输入 watch CalculationDemo add “{params, returnObj}” -s -x 2,我们可以观察方法出参和返回值
IMAGE

那我们怎么使用条件表达式呢?我们输入 watch CalculationDemo add “{params, returnObj}” -x 2 -s params[0]==10
IMAGE
这个条件表达式的意思是,只有当第一个参数的值满足等于10的时候,此命令才会有响应。
如果我们在此程序将条件修改为 params[0] == 2,我们会发现由于条件不成立,则会没有打印。

有时候我们需要过滤导致异常的参数信息,我们可以利用 -e 参数: watch CalculationDemo verify “{params, throwExp}” -e -x 2
IMAGE

trace 命令

trace 命令可以统计整个调用链路的性能开销和链路追踪。trace 在执行的过程中自身会有一定的开销,这个统计有点类似JProfile的功能,但是JProfile 会减去自身执行的开销时间,相对来说会比较准确,而且方法越多,trace 可能偏差越大。虽然有偏差,但是还是能看出不少问题的。
参数说明:

参数名称 参数说明
class-pattern 类名表达式
method-pattern 方法表达式
condition-express 条件表达式
[E] 开启正则表达式
[n:] 命令执行次数
#cost 方法执行耗时(单位毫秒)

使用示例:
进入CalculationDemo 进程后,输入trace CalculationDemo execute
IMAGE
[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
IMAGE

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
34
public 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
IMAGE

我们打印打印number为单数,且执行时间大于5的执行链路
IMAGE

tt命令

tt 命令是TimeTunnel 的缩写。可以记录指定方法每次的调用入参和返回信息。并且能对这些调用进行对比和观察。
我们还是进入StackDemo, 观察sleepByNumber 的调用
参数说明:

参数名称 参数说明
class-pattern 类名表达式
method-pattern 方法表达式
condition-express 条件表达式
[t] 主参数,表面希望记录下类的每次执行情况。
[n] 表示记录的次数
[p] 重新执行调用

使用示例:
进入StackDemo 输入: tt -t -n 30 StackDemo sleepByNumber
IMAGE
字段说明:

字段名称 字段说明
INDEX 索引号,每一次调用对应着一个索引。如果想查看针对某一次调用的信息必须基于此索引编号
TIMESTAMP 方法执行的时候,本地的时间
COST 方法执行的耗时
IS-RET 方法是否以正常返回的形式结束
IS-EXP 方法是否以抛异常的形式结束
OBJECT 执行对象的hashCode ,但跟JVM 的hashcode 并不是同一个东西
CLASS 执行的类名
METHOD 执行的方法名

我们用上面的tt命令记录一大片时间段之后,可以利用条件从中筛选出自己想要看到的记录。
首先我们输入命令 tt -l,查看所有记录
IMAGE

然后我们利用 -s 参数进行检索:tt -s params[0]==5
IMAGE

我们也可以利用-i参数对某一次调用进行查看:tt -i 1001,这个1001就是记录信息里面的INDEX的值。
IMAGE

重做一次调用:有时候我们想要重现其中一次调用,我们可以直接用tt命令 加上-p 参数即可,再也不用前端人员点击按钮出发多一次啦。
1.我们输入 tt -t StaticDemo oddNumber -n 3 记录三次调用
2.然后我们使用 tt -i 1031 -p 触发再一次执行方法。
IMAGE

总结

这篇文章主要介绍了Arthas 类加载相关命令,以及方法监控与跟踪相关命令。特别是方法监控与跟踪在解决线上问题起到非常好的作用,但是有些命令过于强大也要谨慎使用,例如可以让JVM 动态重新加载class 文件的redefine 命令。至此我们对Arthas 3.0版本的命令都有了大致的了解,这的确是一款很不错的线上诊断工具,希望下个版本能解决现有的小不足,增加一些更让我们惊叹的功能。

如果看的爽,不如请我吃根辣条?