什么是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实现。
- 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有多个模块组成,如下图所示:
使用接十分简单,具体可是参考官方文档,这里只是对的原理的解析
这里只要解释核心方法 watch 和trace 的原理
局部变量与操作数栈
我们知道,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 过程中会采坑的。
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;
}