Arthas 解读

前言

什么是JVMTI

JVMTI 的全称是 JVM Tools Interface,是Java虚拟机提供的一整套后门。通过这套后门可以对虚拟机方方面面进行监控,分析,甚至干预虚拟机的运行。JVMTI 本质上是在JVM内部的许多事件进行了埋点。通过这些埋点可以给外部提供当前上下文的一些信息。它是分析工具与调试器的基础。

什么是Instrumentation

虽然java提供了JVMTI,但是对应的agent需要用C/C++开发,对java开发者而言并不是非常友好。因此在Java SE 5的新特性中加入了Instrumentation机制。有了 Instrumentation,开发者可以构建一个基于Java编写的Agent来监控或者操作JVM了,比如替换或者修改某些类的定义等。

JavaAgent 的两种方式

1.在 Java 中实现Instrumentation有两种方式:在类执行前通过premain来执行 , 或者在启动执行后通过agentMain实现。

  1. premain是在 Java SE 5中新引入的 ,开发者只能在 premain 当中施展想象力,所作的 Instrumentation
    也仅限与 main 函数执行前,这样的方式存在一定的局限性。 Java SE 6 针对这种状况做出了改进,开发者可以在 main函数开始执行以后,再启动自己的 Instrumentation 程序,这种方式通过agentMain。

Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪Java代码;实时监控JVM状态。
官方文档地址:https://alibaba.github.io/arthas/
GitHub地址:https://github.com/alibaba/arthas/

arthas有多个模块组成,如下图所示:
Arthas 解读_第1张图片
使用接十分简单,具体可是参考官方文档,这里只是对的原理的解析
这里只要解释核心方法 watch 和trace 的原理

1.watch如何获取到方法的入参类型与值

局部变量与操作数栈
我们知道,Java 代码是在线程内部执行的。每个线程都有自己的执行栈,栈由帧组成。每个帧表示一个方法调用:每次调用一个方法时,会将一个新帧压入当前线程的执行栈。当方法返回时,会将这个帧从执行栈中弹出。

每一帧包括两部分:一个局部变量部分和一个操作数栈部分。局部变量部分包含可根据索引以随机顺序访问的变量。由名字可以看出,操作数栈部分是一个栈,其中包含了供字节代码指令用作操作数的值。用下面例子来说明:

public void add(int x,int y){
    int result = x+y;
}

调用add方法时会创建一个帧,一开始会为该帧的局部变量与操作数栈进行初始化,一般局部变量会将当前 this 押入到第0位置,再将两个方法参数 x , y 分别压入到第1和第2的位置,那么初始化好的局部变量就是:【this , x , y】,操作数栈还是为空:【】

add方法对应的字节码操作为如下:

ILOAD 1 	//1
ILOAD 2 	//2
IADD 		//3
ISTORE 3	//4

需要注意:局部变量部分和操作数栈部分中的每个槽(slot)可以保存除 long 和 double 变量之外的任意 Java 值。long 和 double 变量需要两个槽。如果忽略这点,在实现 watch 过程中会采坑的。

2.trace如何获得一个方法的调用路径

arthas 中 trace 可以监控一个方法内部调用路径,和每个节点上耗时.这不就是Spring中的切面编程,对方法进行加强嘛,注意的是我们生产的虚拟机没有重启,更没有对一个方法加上注解或者通过配置文件来加强这个方法,因此trace是在虚拟机层面上的AOP!!

public int doAdd(int x, int y) throws Exception{
    Thread.sleep(1000);
    test();
    return  x+y;
}

public void test() throws Exception{
    Thread.sleep(1000);
    //System.out.println("");
}

arthas匹配到的函数里的子调用,并不会向下trace多层,因此我们只考虑如何知道doAdd中子调用链路。有些同学可能会想到用 stackTraceElement 来实现:StackTraceElement[] stackTraceElement = Thread.currentThread().getStackTrace();但是getStackTrace只能获取到在调用该语句之前还未出栈的栈帧。
实际上他是 使用visitMethodInsn:访问方法操作指令

//opcode:为INVOKESPECIAL,INVOKESTATIC,INVOKEVIRTUAL,INVOKEINTERFACE;
//owner:方法拥有者的名称;
//name:方法名称;
//descriptor:方法描述,参数和返回值;
//isInterface;是否是接口; 
public void visitMethodInsn(final int opcode,final String owner,
         final String name,final String descriptor,final boolean isInterface) {
    if (api < Opcodes.ASM5) {
      if (isInterface != (opcode == Opcodes.INVOKEINTERFACE)) {
        throw new IllegalArgumentException("INVOKESPECIAL/STATIC on interfaces requires ASM5");
      }
      visitMethodInsn(opcode, owner, name, descriptor);
      return;
    }
    if (mv != null) {
      mv.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    }
  }

通过这个方法,我们可以监控到所有子调用的信息了,但是要注意asm中只有访问方法操作指令,并没有方法结束的调用指令。注意visitInsn通过opcode来判断指令是否为return只能知道当前方法的结束,但是不能获取子调用的结束的方法,这么说可能会有点绕,那上面的doAdd来解释:asm 中的MethodVisitor的确可以通过visitCode对doAdd执行前进行干预,也可以通过visitInsn中的opcode知道doAdd的结束操作。那么一前一后就可以对方法进行监控了啊!!

doAdd(int x, int y){
	记录doAdd方法开始时间 --通过visitCode
        
    记录Thread.sleep方法开始时间 --通过visitMethodInsn
    记录(因为Thrad.sleep前面没有方法了,所以可以随便设置初始方法)方法结束的时间
    Thread.sleep(1000);
	
    记录Thread.slepp方法结束的时间
    记录test方法开始时间 --通过visitMethodInsn
    test();
	
    记录test方法结束的时间
    记录doAdd方法结束时间,打印,结束时间-开始时间 --通过visitInsn中的opcode
	return x+y;
}

你可能感兴趣的:(Arthas 解读)