“真正拉开开发者差距的,从来不是代码量!我是曾续缘,今天带你用架构师思维拆解技术方案,关注我的人都在提升认知维度🧠”
JVMTI
JVMTI(Java Virtual Machine Tool Interface)是一种提供对 Java 虚拟机内部状态访问的原生Native 编程接口,它允许外部工具和代理程序以原生代码的形式与JVM进行交互。这使得开发人员不仅能够调试在该虚拟机上运行的 Java 程序,还能查看它们运行的状态,设置回调函数,控制某些环境变量,从而优化程序性能。需要注意的是,并非所有的JVM实现都支持JVMTI。
JVMTI 是 Java 平台调试体系(Java Platform Debugger Architecture,JPDA)的一部分,位于 JPDA 的最底层。这种机制自 J2SE 5.0 引入,取代了早期的 JVMDI 和 JVMPI 接口,提供了更全面和高效的工具接口。
JVMTI 提供了一组 C 语言 API,允许代理查询和修改 JVM 的状态。这些 API 包括 jvmtiEnv
接口,代理可以通过它访问 JVM 的内部信息。
要使用 JVMTI ,我们需要编写一个 C 或 C++ 程序来实现我们的工具逻辑。这个程序将使用 JVMTI API 与 JVM 交互。接下来需要编译我们的代理代码生成一个共享库(在 Linux 上是 .so
文件,在 Windows 上是 .dll
文件)。
在启动 JVM 时通过 -agentlib
参数指定代理库的位置。
或者使用 jattach
命令或通过 JVM 的 attach
机制来动态加载代理。
由于 JVMTI 主要是基于 C/C++ 的,因此对于习惯使用 Java 开发的开发者来说,调试可能较为困难。
Instrumentation
接口
Instrumentation是Java 6引入的API,位于 java.lang.instrument
包下,旨在简化JVMTI的一些复杂性,并提供更高级别的抽象来支持Java代理(Java agents)。
Instrumentation提供了一组更易于使用的函数,使得Java开发者可以更容易地监控和修改JVM,而不需要深入了解JVMTI的细节。例如,Instrumentation接口中的addTransformer
方法实际上是通过JVMTI的函数来实现的。
要使用Instrumentation,需要创建一个包含 premain
方法或 agentmain
方法的类,这些方法是 Java Agent 的入口点,Instrumentation
接口可作为方法的参数,JVM 会在适当的时候调用它们。
重要方法
以下是一些Instrumentation
接口中常用的方法:
addTransformer(ClassFileTransformer transformer, boolean canRetransform)
: 添加一个类文件转换器。retransformClasses(Class<?>... classes)
: 重新转换指定的类。redefineClasses(ClassDefinition... definitions)
: 重新定义指定的类。getAllLoadedClasses()
: 返回所有已加载的类。getObjectSize(Object objectToSize)
: 返回指定对象及其引用对象的大小。appendToBootstrapClassLoaderSearch(JarFile jarfile)
: 将指定的jar文件添加到引导类加载器的搜索路径。appendToSystemClassLoaderSearch(JarFile jarfile)
: 将指定的jar文件添加到系统类加载器的搜索路径。
Java Agent
Java Agent是一种特殊类型的Java程序,它在JVM(Java虚拟机)启动时或运行时被加载,用于监控和操作JVM内部的一些状态和行为。
这个Java程序是一个jar文件,包含Agent Class和MANIFEST.MF。
Agent Class
Agent Class是指包含premain
或agentmain
方法的Java类。这个类是Java Agent的核心,它负责定义当Agent被加载到JVM时应该执行的操作。
通常,一个Agent Class只包含一个premain
方法或一个agentmain
方法,或者两者都有。
Agent Class必须打包在一个jar文件中,并且这个jar文件需要在MANIFEST.MF文件中指明Agent Class。
premain
方法
premain
方法是在JVM启动过程中,在应用程序的main
方法执行之前被调用的方法。它的目的是在应用程序的任何类被加载之前,对JVM进行初始化设置。
premain
方法有两种可能的签名:
public static void premain(String agentArgs, Instrumentation inst);
或者
public static void premain(String agentArgs);
如果两个版本都存在,优先使用带有Instrumentation
参数的方法。
agentmain
方法
agentmain
方法允许在JVM运行时动态地加载Agent。这意味着应用程序已经启动并且可能已经运行了一段时间,然后Agent被附加到JVM上。
agentmain
方法的签名如下:
public static void agentmain(String agentArgs, Instrumentation inst);
或者
public static void agentmain(String agentArgs);
如果两个版本都存在,优先使用带有Instrumentation
参数的方法。
MANIFEST.MF文件
MANIFEST.MF文件是一个多行文本文件,位于jar文件的META-INF目录下。它包含关于jar文件和其内容的元数据。
以下是MANIFEST.MF文件的一个基本示例,它指明了Premain-Class和Agent-Class属性:
Manifest-Version: 1.0
Premain-Class: com.example.MyAgent
Agent-Class: com.example.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
以下是每个属性的说明:
Manifest-Version
: 指示manifest文件的版本,通常是1.0。Premain-Class
: 指定包含premain
方法的类的全限定名。这个类会在JVM启动时加载Agent之前被调用。Agent-Class
: 指定包含agentmain
方法的类的全限定名。这个类会在JVM运行时动态加载Agent时被调用。Can-Redefine-Classes
: 如果设置为true,则允许重新定义类。这是在Instrumentation
接口中使用redefineClasses
方法所必需的。Can-Retransform-Classes
: 如果设置为true,则允许重新转换类。这是在Instrumentation
接口中使用retransformClasses
方法所必需的。
创建Agent的步骤
创建 Agent Class
首先,我们需要编写一个 Java 类,该类包含 premain
方法(如果希望在 JVM 启动时加载)或 agentmain
方法(如果希望在 JVM 运行时加载)。
例如,下面是一个简单的 Agent Class 示例:
import java.lang.instrument.Instrumentation;
public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("MyAgent premain method called with args: " + agentArgs);
// 在这里可以添加字节码操作或其他初始化逻辑
// 注册字节码转换器
inst.addTransformer(new MyTransformer());
}
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("MyAgent agentmain method called with args: " + agentArgs);
// 如果实现了 agentmain 方法,那么它会在 JVM 运行时动态加载 agent 时被调用
}
}
public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 对字节码进行操作
return transformedBytes;
}
}
编译 Java 类
编译上面的 Java 类,生成 .class
文件。假设我们的源代码文件名为 MyAgent.java
,可以使用以下命令进行编译:
javac MyAgent.java
这将会生成 MyAgent.class
文件。
创建 MANIFEST.MF 文件
创建一个目录结构来存放编译后的类文件和MANIFEST.MF文件。例如:
myagent/
├── com/
│ └── example/
│ └── MyAgent.class
└── META-INF/
└── MANIFEST.MF
创建一个 MANIFEST.MF
文件,并在其中指定 Agent Class 名称。这个文件应该放在 JAR 文件的 META-INF/MANIFEST.MF
路径下。内容如下:
Manifest-Version: 1.0
Agent-Class: com.example.MyAgent
Premain-Class: com.example.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: false
在这里,Agent-Class
行指定了 Agent Class 的完全限定名(包括包名)。
打包为 JAR 文件
使用 jar
命令将所有的 .class
文件以及 MANIFEST.MF
文件打包成一个 JAR 文件。例如:
jar cf MyAgent.jar MyAgent.class META-INF/
这将创建一个名为 MyAgent.jar
的 JAR 文件,其中包含了 Agent Class 以及 MANIFEST 文件。
启动Agent
现在我们有了一个包含正确配置的 JAR 文件,可以通过 -javaagent
参数来启动 JVM 并加载你的 Java Agent。例如:
java -javaagent:/path/to/MyAgent.jar=option=value -jar yourapp.jar
在这里,/path/to/MyAgent.jar
是我们的 JAR 文件的路径,option=value
是传递给 premain
方法的参数。
Java Agent的加载分为两种情况:在JVM启动时加载和在JVM运行时动态加载。以下是这两种情况的详细说明。
JVM启动时加载
在JVM启动时加载Agent是通过-javaagent
命令行选项来实现的。
在启动JVM时,使用-javaagent
选项指定Agent jar文件的位置。
java -javaagent:myagent.jar -jar MyApp.jar
或者,如果你是在命令行启动Java应用程序:
java -javaagent:myagent.jar com.example.MyApp
在这里,myagent.jar
是包含Agent的jar文件,MyApp.jar
或com.example.MyApp
是你要启动的应用程序。
可以通过-javaagent
选项多次指定多个Agent。
JVM运行时动态加载
动态加载Agent允许我们在JVM已经运行的情况下加载Agent,而不需要重启JVM。
我们的Agent类应该实现一个agentmain
方法,用于在Agent被动态加载时执行。
public class MyAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
// 执行操作
}
}
按照启动时加载Agent的步骤,创建包含Agent-Class
属性的MANIFEST.MF
文件,并将Agent类打包到jar文件中。
使用com.sun.tools.attach
包中的VirtualMachine
类来attach到目标JVM并加载Agent。
import com.sun.tools.attach.*;
public class AgentLoader {
public static void main(String[] args) throws Exception {
// 获取目标JVM的PID
String pid = "1234"; // 需要替换为目标JVM的PID
// Attach到目标JVM
VirtualMachine vm = VirtualMachine.attach(pid);
// 加载Agent
vm.loadAgent("/path/to/myagent.jar", "arguments");
// Detach
vm.detach();
}
}
Attach 接口
Attach 接口是 Java 虚拟机(JVM)提供的一个用于动态加载 Java Agent 的机制。通过 Attach 接口,可以在 Java 虚拟机(JVM)运行时附加一个 Java Agent。
JDK 自带的一些命令,如 jstack(用于打印线程栈)、jps(用于列出Java进程)和 jmap(用于进行内存dump),都是通过Attach API实现的。
进程间通信
Attach API利用进程间通信机制(Inter-Process Communication, IPC)来实现外部进程与JVM之间的交互。以下是进程间通信的过程:
- Socket连接:Attach API通过创建一个socket连接到目标JVM的Attach Listener线程。这个连接是建立在TCP或UNIX域socket之上的,具体取决于操作系统。
- 指令发送:一旦连接建立,外部进程就可以通过这个socket发送指令给JVM。这些指令可以是加载Java Agent、获取JVM信息、触发垃圾回收等。
- 结果返回:JVM在接收到指令后,会执行相应的操作,并将结果返回给外部进程。这种通信是双向的,确保了外部进程可以与JVM进行有效的交互。
信号监听线程
在JVM内部,有一个名为“Signal Dispatcher”的线程,它在JVM启动时被创建,专门用于监听操作系统发出的信号。
当操作系统向JVM进程发送特定信号时,Signal Dispatcher线程会捕获这些信号并进行处理。例如,在某些操作系统中,发送特定的信号可以触发JVM执行附加(attach)或分离(detach)操作。
在Attach机制中,当外部进程想要连接到JVM时,它可能会通过操作系统发送一个信号给JVM。JVM接收到这个信号后,会唤醒Attach Listener线程,准备接受即将到来的socket连接请求。
Attach工作流程
- 发现JVM实例:外部进程通过操作系统的进程管理功能来查找目标JVM的PID。
- 发送信号:外部进程通过操作系统发送一个信号给目标JVM。这个信号会由Signal Dispatcher线程接收。
- 建立连接:Signal Dispatcher线程唤醒Attach Listener线程,该线程随后会监听来自外部进程的socket连接请求。
- 验证和授权:一旦socket连接建立,JVM会进行安全检查,确保外部进程有权限执行后续操作。
- 执行操作:通过验证的外部进程现在可以通过Attach API发送指令,JVM执行这些指令并返回结果。
Agent工作流程
Java Agent 利用 Instrumentation
API 和 ClassFileTransformer
实现类加载时的字节码修改,从而达到 AOP、性能监控、安全检查等目的。
- JVM启动:当JVM启动时,它会加载一个java agent的class文件。
- 执行premain()方法:在java agent的class被加载之后,会执行该类中的premain()方法。这是agent的主要入口点。
- Instrumentation API的使用:在premain()方法内部,会调用Instrumentation API来对字节码进行操作。
- 添加ClassFileTransformer:通过Instrumentation API,可以注册一个ClassFileTransformer实例,用于转换类的字节码。
- JVMTI监听事件:JVMTI(Java Virtual Machine Tool Interface)会被用来监听类加载事件。
- 回调ClassFileTransformer.transform()方法:每当一个新的类被加载时,之前注册的ClassFileTransformer的transform()方法就会被回调。
- 修改未加载的类:在transform()方法中,可以对尚未加载到内存中的类进行修改或替换。
- AppClassLoader或其他ClassLoader加载类:经过可能的修改后,类最终由AppClassLoader或其他ClassLoader加载到JVM中。
- 使用已加载的类:一旦类被成功加载,就可以在应用程序中使用这些类。
- 其他ClassLoader加载类:除了AppClassLoader之外,还有其他的ClassLoader可能参与类的加载过程。
- 每次加载新类前回调:对于每个新的类加载尝试,都会再次触发ClassFileTransformer的transform()方法的回调。
- 返回修改后的类:transform()方法处理完毕后,会将修改后的类返回给JVM继续后续的加载和使用过程。
- 结束premain()方法并执行main()方法:premain()方法结束后,程序的主线程会开始执行main()方法。
在整个过程中,Instrumentation
API 起到了关键的作用,它负责协调 Agent 和应用程序之间的关系,使 Agent 可以在类加载时进行干预。ClassFileTransformer
是一个重要的组件,它实现了字节码的转换逻辑,可以根据需求修改类的字节码。而Instrumentation
接口实际上是基于 JVMTI 实现的。
参考连接:
https://juejin.cn/post/7157684112122183693#heading-4
参考文章:https://cengxuyuan.cn