一、JPDA简介
JPDA is a multi-tiered debugging architecture that allows tools developers to easily create debugger applications which run portably across platforms, virtual machine (VM) implementations and JDK versions.---文档原介绍, 我理解JPDA就是对Java整个调试和监控体系的集成,我们实际上开发时涉及到的所有应用程序监控和调试的渠道都由JPDA体系当中提供的标准接口组成。然后整个调试和监控的过程都需要遵循JPDA体系制定的规范。
JPDA由三个部分组成,分别为两套接口(JVMTI、JDI接口)和一个协议(JDWP协议)组成。架构图如下图所示(摘自Oracle官网)。
JPDA的三个组成部分按照抽象级别由低到高分别为JVMTI、JDWP、JDI。
1、JVMTI(Java Virtual Machine Tool Interface)
JVMTI介绍
JVMTI(JVM Tool Interface) 是一套native编程接口,位于jpda 最底层,它提供了检查在JVM中的应用程序状态、控制应用程序执行的渠道。JVMTI可以提供性能分析、debug、内存管理、线程分析等功能。 比如现在集团用的Arthas、(chaosblade-jvm)Java相关的故障注入功能、SkyWalking,包括我们的JDK自带的一些工具jstack、jconsole、jvisualvm等等底层都是通过JVMTI来进行实现的。
JVMTI构成
JVMTI它是由一堆jvmEvent组成。这些代码在jvm源码中才能看见,那么我们就得通过下载openJDK来观看JVM源码,在openjdk里的jvmti.h或者jvmti.xml文件中进行查看。实际上这些jvmEvent都存在一个结构体变量当中,如下图所示。这些事件根据性质又分为JVMPI和JVMDI, 也就是监控和调试接口。在Java5中JVMTI将这两套接口进行了合并。
JVMTI原理
在文档介绍中也说了是一套native编程接口,那么我们要使用这些接口的话就得去实现这些接口。JVMTI是基于事件驱动的,JVM会对内部的很多操作做一个事件埋点,每执行到这些操作JVM就会主动调用一些事件的回调接口(比如线程启动、类加载、作者回收、异常事件等等),这些接口可以供开发者扩展自己的逻辑, 一般地,我们需要实现一个Agent使用JVMTI的功能,具体是在Agent里面对JVMTI声明的jvmtiEvent事件做回调事件的注册。然后把这个Agent编译成动态链接库然后供Java程序来加载并进行使用。加载的时机分为两种: 程序启动之前和程序启动之后。
对于这个Agent的实现是需要涉及到C和C++代码的,虽然作者精通C和C++的拼写,但是这并不合适出现在文章中,实际上在Java5以后我们安装java环境后已经给我们实现了一个Agent动态链接库,这个库在我们每个安装了java环境的电脑下的jre/lib目录下如下图所示(这个库的源码就在jvm源码中对应invocationAdapter.c,后续会进行分析)。
有了这个动态链接库以后,我们写的JavaAgent就能够完成对JVMTI事件的回调注册了,这个机制叫做Java Instrument。我们顺着Java Instrument机制的原理来完成对JVMTI知识的学习。
Java Instrument介绍
Java Instrument是Java SE5的新特性, 通过这个机制Java编程人员可以构建一个独立于应用程序的Agent,借助这个Agent我们可以监测、调试甚至修改目标应用程序的运行。这个Agent会以一种jar包的形式供目标程序来调用。通过Agent我们就可以实现一种虚拟机级别的AOP解决方案, 相较于传统的jdk动态代理、cglibAOP方案来说,Instrument机制是完全无侵入式的,并且支持在目标应用程序运行以后再进行动态aop的操作。相较而言这种AOP机制适用范围更广,并且更加实用。
Java Instrument使用和原理
Java Instrument支持premain和agentmain两种方式来实现Agent的注入。区别在于注入的时机在目标程序启动之前还是目标程序启动之后,然后他们其底层操作其实都是触发对JVMTI的ClassFileLoadHook这个事件的回调来实现的。
premain: 如果使用premain来实现agent注入,那么我们在启动前要给程序设置一些命令参数,让程序在加载前调用agent, 那么在启动程序后我们可以达成一些我们对程序想要做的aop操作。就我个人理解而言,这种方式的aop比较轻量,但是没啥应用场景。
agentmain:它是在应用程序启动后我们还想对应用程序做一些aop操作,这个的话cglib、jdk动态代理肯定都是做不到的,实际上我们会启动一个进程去和目标应用程序的jvm进行通信,然后把我们的实现的Agent代码attach到目标jvm当中。通过进程通信的方式来让目标jvm解析通信请求并且执行相关的方法。这种方式的aop需要另启一个进程。在介绍方面讲的实践场景也基本都是通过agentmain来做的。
premain实践:
1、附正常代码,执行TestTransClass的Main()函数控制台会不停地打印111。
// TransClass.java
public class TransClass {
public TransClass() {
}
public int getNumber() {
return 111;
}
}
public class TestTransClass {
public static void main(String[] args) throws InterruptedException {
TransClass transClass = new TransClass();
while (true) {
Thread.sleep(2000);
System.out.println("print number = " + transClass.getNumber());
}
}
}
2、开启premain agent注入,先编译一个输入222的TransClass字节码文件,然后写一个Agent程序(我这里叫AgentMain), 利用这个Agent程序我们可以完成对字节码文件的增强转换, 这个增强转换的实现是由我们自己来做的, 具体是写一个Transformer类实现ClassFileTransformer接口,并重写transform()方法来完成。在这里我们把getNumber()方法转成了输出222。