Java JVMTI和Instrumention机制介绍

2024-09-04 17:18

本文主要是介绍Java JVMTI和Instrumention机制介绍,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 1、JVMTI 介绍
      • 1.1 Agent的工作形式
      • 1.2 JDPA 相关介绍
    • 2、Instrumention 机制
      • 2.1 Instrumention支持的功能
      • 2.2 基于Instrumention开发一个Agent
        • 2.2.1 编写premain函数
        • 2.2.2 打成jar包
        • 2.2.3 编写测试类
      • 2.3 如何在运行时加载agent
    • 3、参考资料

1、JVMTI 介绍

JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 native 编程接口,是 JVMPI(Java Virtual Machine Profiler Interface)和 JVMDI(Java Virtual Machine Debug Interface)的替代版本。

JVMTI可以用来开发并监控虚拟机,可以查看JVM内部的状态,并控制JVM应用程序的执行。可实现的功能包括但不限于:调试、监控、线程分析、覆盖率分析工具等。

另外,需要注意的是,并非所有的JVM实现都支持JVMTI。

JVMTI只是一套接口,我们要开发JVM工具就需要写一个Agent程序来使用这些接口。Agent程序其实就是一个C/C++语言编写的动态链接库。这里不详细介绍如何开发一个JVMTI的agent程序。感兴趣的可以点击文章末尾的链接查看。

我们通过JVMTI开发好agent程序后,把程序编译成动态链接库,之后可以在jvm启动时指定加载运行该agent。

-agentlib:<agent-lib-name>=<options>

之后JVM启动后该agent程序就会开始工作。

1.1 Agent的工作形式

agent启动后是和JVM运行在同一个进程,大多agent的工作形式是作为服务端接收来自客户端的请求,然后根据请求命令调用JVMTI的相关接口再返回结果。

很多java监控、诊断工具都是基于这种形式来工作的。如果arthas、jinfo、brace等。

另外,我们熟知的java调试也是其实也是基于这种工作原理。

1.2 JDPA 相关介绍

无论我们在开发调试时,都会用到调试工具。其实我们用的所有调试工具其底层都是基于JVMTI的调用。JVMTI本身就提供了关于调试程序的一系列接口,我们只需要编写agent就可以开发一套调试工具了。

虽然对应的接口已经有了,但是要基于这些接口开发一套完整的调试工具还是有一定工作量的。为了避免重复造轮子,sun公司定义了一套完整独立的调试体系,也就是JDPA。

JDPA由3个模块组成:

  1. JVMTI,即底层的相关调试接口调用。sun公司提供了一个 jdwp.dll( jdwp.so)动态链接库,就是我们上面说的agent实现。
  2. JDWP(Java Debug Wire Protocol),定义了agent和调试客户端之间的通讯交互协议。
  3. JDI(Java Debug Interface),是由Java语言实现的。有了这套接口,我们就可以直接使用java开发一套自己的调试工具。

其实有了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。后面还指定了一些参数:

  • transport=dt_socket,表示用监听socket端口的方式来建立连接,这里也可以选择dt_shmem共享内存方式,但限于windows机器,并且服务端和客户端位于一台机器上
  • server=y 表示当前是调试服务端,=n表示当前是调试客户端
  • suspend=n 表示启动时不中断(如果启动时中断,一般用于调试启动不了的问题)
  • address=8000 表示本地监听8000端口

2、Instrumention 机制

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

2.1 Instrumention支持的功能

Instrumention支持的功能都在java.lang.instrument.Instrumentation接口中体现:

public interface Instrumentation {//添加一个ClassFileTransformer//之后类加载时都会经过这个ClassFileTransformer转换void addTransformer(ClassFileTransformer transformer, boolean canRetransform);void addTransformer(ClassFileTransformer transformer);//移除ClassFileTransformerboolean 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 的区别:

  1. transform是对类的byte流进行读取转换的过程,需要先获取类的byte流然后做修改。而redefineClasses更简单粗暴一些,它需要直接给出新的类byte流,然后替换旧的
  2. transform可以添加很多个,retransformClasses 可以让指定的类重新经过这些transform做转换。

2.2 基于Instrumention开发一个Agent

利用java.lang.instrument包下面的相关类,我们可以开发一个自己的Agent程序。

2.2.1 编写premain函数

编写一个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 {// preconditionFile file = new File(fileName);InputStream is = new FileInputStream(file);long length = file.length();byte[] bytes = new byte[(int) length];// Read in the bytesint 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);}
}
2.2.2 打成jar包

之后我们把上面两个类打成一个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>
2.2.3 编写测试类

上面的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已经被转换了

2.3 如何在运行时加载agent

上面开发的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成功运行。

3、参考资料

Java Attach机制简介

基于Java Instrument的Agent实现

IBM: Instrumentation 新功能

Instrumentation 中redefineClasses 和 retransformClasses 的区别

JVMTI开发文档

JVMTI oracle 官方文档

JVMTI和JDPA介绍

这篇关于Java JVMTI和Instrumention机制介绍的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/1136538

相关文章

Spring Boot中WebSocket常用使用方法详解

《SpringBoot中WebSocket常用使用方法详解》本文从WebSocket的基础概念出发,详细介绍了SpringBoot集成WebSocket的步骤,并重点讲解了常用的使用方法,包括简单消... 目录一、WebSocket基础概念1.1 什么是WebSocket1.2 WebSocket与HTTP

SpringBoot+Docker+Graylog 如何让错误自动报警

《SpringBoot+Docker+Graylog如何让错误自动报警》SpringBoot默认使用SLF4J与Logback,支持多日志级别和配置方式,可输出到控制台、文件及远程服务器,集成ELK... 目录01 Spring Boot 默认日志框架解析02 Spring Boot 日志级别详解03 Sp

java中反射Reflection的4个作用详解

《java中反射Reflection的4个作用详解》反射Reflection是Java等编程语言中的一个重要特性,它允许程序在运行时进行自我检查和对内部成员(如字段、方法、类等)的操作,本文将详细介绍... 目录作用1、在运行时判断任意一个对象所属的类作用2、在运行时构造任意一个类的对象作用3、在运行时判断

java如何解压zip压缩包

《java如何解压zip压缩包》:本文主要介绍java如何解压zip压缩包问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java解压zip压缩包实例代码结果如下总结java解压zip压缩包坐在旁边的小伙伴问我怎么用 java 将服务器上的压缩文件解压出来,

SpringBoot中SM2公钥加密、私钥解密的实现示例详解

《SpringBoot中SM2公钥加密、私钥解密的实现示例详解》本文介绍了如何在SpringBoot项目中实现SM2公钥加密和私钥解密的功能,通过使用Hutool库和BouncyCastle依赖,简化... 目录一、前言1、加密信息(示例)2、加密结果(示例)二、实现代码1、yml文件配置2、创建SM2工具

Spring WebFlux 与 WebClient 使用指南及最佳实践

《SpringWebFlux与WebClient使用指南及最佳实践》WebClient是SpringWebFlux模块提供的非阻塞、响应式HTTP客户端,基于ProjectReactor实现,... 目录Spring WebFlux 与 WebClient 使用指南1. WebClient 概述2. 核心依

Spring Boot @RestControllerAdvice全局异常处理最佳实践

《SpringBoot@RestControllerAdvice全局异常处理最佳实践》本文详解SpringBoot中通过@RestControllerAdvice实现全局异常处理,强调代码复用、统... 目录前言一、为什么要使用全局异常处理?二、核心注解解析1. @RestControllerAdvice2

Spring IoC 容器的使用详解(最新整理)

《SpringIoC容器的使用详解(最新整理)》文章介绍了Spring框架中的应用分层思想与IoC容器原理,通过分层解耦业务逻辑、数据访问等模块,IoC容器利用@Component注解管理Bean... 目录1. 应用分层2. IoC 的介绍3. IoC 容器的使用3.1. bean 的存储3.2. 方法注

Spring事务传播机制最佳实践

《Spring事务传播机制最佳实践》Spring的事务传播机制为我们提供了优雅的解决方案,本文将带您深入理解这一机制,掌握不同场景下的最佳实践,感兴趣的朋友一起看看吧... 目录1. 什么是事务传播行为2. Spring支持的七种事务传播行为2.1 REQUIRED(默认)2.2 SUPPORTS2

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.