也能够看个人CSDN上的博客:
https://blog.csdn.net/u013332124/article/details/88367630html
JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口,是 JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface)的替代版本。java
JVMTI能够用来开发并监控虚拟机,能够查看JVM内部的状态,并控制JVM应用程序的执行。可实现的功能包括但不限于:调试、监控、线程分析、覆盖率分析工具等。程序员
另外,须要注意的是,并不是全部的JVM实现都支持JVMTI。apache
JVMTI只是一套接口,咱们要开发JVM工具就须要写一个Agent程序来使用这些接口。Agent程序其实就是一个C/C++语言编写的动态连接库。这里不详细介绍如何开发一个JVMTI的agent程序。感兴趣的能够点击文章末尾的连接查看。编程
咱们经过JVMTI开发好agent程序后,把程序编译成动态连接库,以后能够在jvm启动时指定加载运行该agent。windows
-agentlib:<agent-lib-name>=<options>
以后JVM启动后该agent程序就会开始工做。缓存
agent启动后是和JVM运行在同一个进程,大多agent的工做形式是做为服务端接收来自客户端的请求,而后根据请求命令调用JVMTI的相关接口再返回结果。oracle
不少java监控、诊断工具都是基于这种形式来工做的。若是arthas、jinfo、brace等。app
另外,咱们熟知的java调试也是其实也是基于这种工做原理。jvm
不管咱们在开发调试时,都会用到调试工具。其实咱们用的全部调试工具其底层都是基于JVMTI的调用。JVMTI自己就提供了关于调试程序的一系列接口,咱们只须要编写agent就能够开发一套调试工具了。
虽然对应的接口已经有了,可是要基于这些接口开发一套完整的调试工具仍是有必定工做量的。为了不重复造轮子,sun公司定义了一套完整独立的调试体系,也就是JDPA。
JDPA由3个模块组成:
[图片上传失败...(image-3bb125-1552119475529)]
其实有了jdwp Agent以及知道了交互的消息协议格式,咱们就能够基于这些开发一套调试工具了。可是相对仍是比较费时费力,因此才有了JDI的诞生,JDI是一套JAVA API。这样对于不熟悉C/C++的java程序员也能开发本身的调试工具了。
另外,JDI 不只能帮助开发人员格式化 JDWP 数据,并且还能为 JDWP 数据传输提供队列、缓存等优化服务
再回头看一下启动JVM debug时须要带上的参数:
java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 -jar test.jar
jdwp.dll做为一个jvm内置的agent,不须要上文说的-agentlib来启动agent。这里经过-Xrunjdwp
来启动该agent。后面还指定了一些参数:
虽然java提供了JVMTI,可是对应的agent须要用C/C++开发,对java开发者而言并非很是友好。所以在Java SE 5的新特性中加入了Instrumentation机制。有了 Instrumentation,开发者能够构建一个基于Java编写的Agent来监控或者操做JVM了,好比替换或者修改某些类的定义等。
Instrumention支持的功能都在java.lang.instrument.Instrumentation
接口中体现:
public interface Instrumentation { //添加一个ClassFileTransformer //以后类加载时都会通过这个ClassFileTransformer转换 void addTransformer(ClassFileTransformer transformer, boolean canRetransform); void addTransformer(ClassFileTransformer transformer); //移除ClassFileTransformer boolean removeTransformer(ClassFileTransformer transformer); boolean isRetransformClassesSupported(); //将一些已经加载过的类从新拿出来通过注册好的ClassFileTransformer转换 //retransformation能够修改方法体,可是不能变动方法签名、增长和删除方法/类的成员属性 void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; boolean isRedefineClassesSupported(); //从新定义某个类 void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException; boolean isModifiableClass(Class<?> theClass); @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); @SuppressWarnings("rawtypes") Class[] getInitiatedClasses(ClassLoader loader); long getObjectSize(Object objectToSize); void appendToBootstrapClassLoaderSearch(JarFile jarfile); void appendToSystemClassLoaderSearch(JarFile jarfile); boolean isNativeMethodPrefixSupported(); void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix); }
咱们经过addTransformer方法注册了一个ClassFileTransformer,后面类加载的时候都会通过这个Transformer处理。对于已加载过的类,能够调用retransformClasses来从新触发这个Transformer的转换。
ClassFileTransformer能够判断是否须要修改类定义并根据本身的代码规则修改类定义而后返回给JVM。利用这个Transformer类,咱们能够很好的实现虚拟机层面的AOP。
redefineClasses 和 retransformClasses 的区别:
- transform是对类的byte流进行读取转换的过程,须要先获取类的byte流而后作修改。而redefineClasses更简单粗暴一些,它须要直接给出新的类byte流,而后替换旧的。
- transform能够添加不少个,retransformClasses 可让指定的类从新通过这些transform作转换。
利用java.lang.instrument包下面的相关类,咱们能够开发一个本身的Agent程序。
编写一个java类,不用继承或者实现任何类,直接实现下面两个方法中的任一方法:
//agentArgs是一个字符串,会随着jvm启动设置的参数获得 //inst就是咱们须要的Instrumention实例了,由JVM传入。咱们能够拿到这个实例后进行各类操做 public static void premain(String agentArgs, Instrumentation inst); [1] public static void premain(String agentArgs); [2]
其中,[1] 的优先级比 [2] 高,将会被优先执行,[1] 和 [2] 同时存在时,[2] 被忽略。
编写一个PreMain:
public class PreMain { public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException { inst.addTransformer(new MyTransform()); } }
MyTransform是咱们本身定义的一个ClassFileTransformer实现类,这个类遇到com/yjb/Test
类,就会进行类定义转换。
public class MyTransform implements ClassFileTransformer { public static final String classNumberReturns2 = "/tmp/Test.class"; public static byte[] getBytesFromFile(String fileName) { try { // precondition File file = new File(fileName); InputStream is = new FileInputStream(file); long length = file.length(); byte[] bytes = new byte[(int) length]; // Read in the bytes int offset = 0; int numRead = 0; while (offset < bytes.length && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { offset += numRead; } if (offset < bytes.length) { throw new IOException("Could not completely read file " + file.getName()); } is.close(); return bytes; } catch (Exception e) { System.out.println("error occurs in _ClassTransformer!" + e.getClass().getName()); return null; } } /** * 参数: * loader - 定义要转换的类加载器;若是是引导加载器,则为 null * className - 彻底限定类内部形式的类名称和 The Java Virtual Machine Specification 中定义的接口名称。例如,"java/util/List"。 * classBeingRedefined - 若是是被重定义或重转换触发,则为重定义或重转换的类;若是是类加载,则为 null * protectionDomain - 要定义或重定义的类的保护域 * classfileBuffer - 类文件格式的输入字节缓冲区(不得修改) * 返回: * 一个格式良好的类文件缓冲区(转换的结果),若是未执行转换,则返回 null。 * 抛出: * IllegalClassFormatException - 若是输入不表示一个格式良好的类文件 */ public byte[] transform(ClassLoader l, String className, Class<?> c, ProtectionDomain pd, byte[] b) throws IllegalClassFormatException { System.out.println("transform class-------" + className); if (!className.equals("com/yjb/Test")) { return null; } return getBytesFromFile(targetClassPath); } }
以后咱们把上面两个类打成一个jar包,并在其中的META-INF/MAINIFEST.MF属性当中加入” Premain-Class”来指定成上面的PreMain类。
咱们能够用maven插件来作到自动打包并写MAINIFEST.MF:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <executions> <execution> <goals> <goal>single</goal> </goals> <phase>package</phase> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifestEntries> <Premain-Class>com.yjb.PreMain</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> <Specification-Title>${project.name}</Specification-Title> <Specification-Version>${project.version}</Specification-Version> <Implementation-Title>${project.name}</Implementation-Title> <Implementation-Version>${project.version}</Implementation-Version> </manifestEntries> </archive> </configuration> </execution> </executions> </plugin>
上面的agent会转换com/yjb/Test
类,咱们就编写一个Test类进行测试。
public class Test { public void print() { System.out.println("A"); } }
先编译这个类,而后把Test.class 放到 /tmp 下。
以后再修改这个类:
public class Test { public void print() { System.out.println("B"); } public static void main(String[] args) throws InterruptedException { new Test().print(); } }
以后运行时指定加上JVM参数 -javaagent:/toPath/agent-jar-with-dependencies.jar
就会发现Test已经被转换了。
上面开发的agent须要启动就必须在jvm启动时设置参数,但不少时候咱们想要在程序运行时中途插入一个agent运行。在Java 6的新特性中,就能够经过Attach的方式去加载一个agent了。
关于Attach的机制原理能够看个人这篇博客:
https://blog.csdn.net/u013332124/article/details/88362317
使用这种方式加载的agent启动类须要实现这两种方法中的一种:
public static void agentmain (String agentArgs, Instrumentation inst); [1] public static void agentmain (String agentArgs);[2]
和premain同样,[1] 比 [2] 的优先级高。
以后要在META-INF/MAINIFEST.MF属性当中加入” AgentMain-Class”来指定目标启动类。
咱们能够在上面的agent项目中加入一个AgentMain类
public class AgentMain { public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException, InterruptedException { //这里的Transform仍是使用上面定义的那个 inst.addTransformer(new MyTransform(), true); //因为是在运行中才加入了Transform,所以须要从新retransformClasses一下 Class<?> aClass = Class.forName("com.yjb.Test"); inst.retransformClasses(aClass); System.out.println("Agent Main Done"); } }
仍是把项目打包成agent-jar-with-dependencies.jar
。
以后再编写一个类去attach目标进程并加载这个agent
public class AgentMainStarter { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { //这个pid填写具体要attach的目标进程 VirtualMachine attach = VirtualMachine.attach("pid"); attach.loadAgent("/toPath/agent-jar-with-dependencies.jar"); attach.detach(); System.out.println("over"); } }
以后修改一下Test类,让他不断运行下去
public class Test { private void print() { System.out.println("1111"); } public static void main(String[] args) throws InterruptedException { Test test = new Test(); while (true) { test.print(); Thread.sleep(1000L); } } }
运行Test一段时间后,再运行AgentMainStarter类,会发现输出变成了最先编译的那个/tmp/Test.class下面的"A"了。说明咱们的agent进程已经在目标JVM成功运行。