目录
1、什么是Instrumentation?
2、底层机制
2.1、工作流程
2.2、Instrumentation API
3、加载Java Agent
3.1、静态Agent示例
3.1.1、定义一个agent
3.1.2、配置 MANIFEST.MF
3.1.3、定义main测试类
3.1.4、启动参数添加-javaagent
3.2、动态Agent示例
3.2.1、定义一个动态 Agent
3.2.2、配置 MANIFEST.MF
3.2.3、定义main测试类
3.2.4、运行结果
3.2.5、总结遇到的问题
4、总结
1、什么是Instrumentation?
Instrumentation 是 Java SE 5 引入的一套 API,它允许开发者在运行时修改类的字节码。Java Instrumentation 可以实现在方法插入额外的字节码从而达到收集使用中的数据到指定工具的目的。Java.lang.instrument包的最大功能就是可以在已有的类上附加(修改)字节码来实现增强的逻辑,它最常见的用途包括:
- 性能监控:对代码进行埋点,以收集性能指标。
- 代码注入:动态添加方法或修改方法的行为。
- 内存分析:通过 Instrumentation 获取对象的大小等信息。
Instrumentation 通常与 Java Agent 一起使用。Java Agent 是一种特殊的 Java 应用,它会在目标应用启动时被 JVM 加载。Agent 通过实现 java.lang.instrument.Instrumentation 接口,来对字节码进行修改或增强。
2、底层机制
Instrumentation 的底层核心在于 JVM Tool Interface (JVMTI)。JVMTI 是 JVM 提供的一组 native 方法,算是JVM暴露出来的一些供用户扩展的接口集合,它允许外部工具与 JVM 进行交互(基于事件驱动,JVM指定到每一层逻辑层都会调用事件的回调接口)。通过 JVMTI,我们可以实现对 JVM 的监控、调试和修改。Instrumentation 就是利用 JVMTI 来实现对字节码的动态修改。
在Java 1.5开始引入Instrument增加技术时,最常用的一种使用方式是通过JVM启动参数:-javaagent来启动,这实际上是一种静态的代理。这种静态的agent只能在jar包启动时候进行代理,存在较大的局限性。Java 1.6开始引入了动态的Attach方式,可以在JVM启动之后的任意时刻通过Attach API远程加载Agent的jar,比如阿里开源的arthas工具就是基于Attach API实现的。
2.1、工作流程
Instrumentation 的实现依赖以下 JVM 的特性和机制:
- 类加载机制:JVM 在加载类时,经过类加载器(ClassLoader)和类验证器(Verifier),将 .class 文件中的字节码加载到内存中。Instrumentation 能够在类加载前修改字节码,或者重新定义已加载的类。
- Instrumentation 接口:JVM 在启动时会初始化一个 InstrumentationImpl 对象,并将其传递给 Java Agent 的 premain 方法或 agentmain 方法。这个对象负责与 JVM 内部的类加载机制交互。
- 字节码转换:Instrumentation 的关键是 ClassFileTransformer 接口,通过该接口,开发者可以拦截类加载过程,并对字节码进行修改。
- 类重定义与重新转换:Instrumentation 提供了 redefineClasses 和 retransformClasses 方法,分别用于重新定义和重新转换类的字节码。
Instrumentation 的工作流程大致如下:
- Agent 加载: JVM 启动时,通过 -javaagent 参数加载指定的 Agent jar 包。
- Agent 初始化: Agent 的 premain 方法被调用,此时 JVM 会传递一个 Instrumentation 实例给 Agent。
- 注册 ClassFileTransformer: Agent 通过 Instrumentation.addTransformer() 方法注册一个 ClassFileTransformer。
- 类加载: 当类加载器加载某个类时,JVM 会调用注册的 ClassFileTransformer 的 transform 方法。
- 字节码修改: ClassFileTransformer 可以对字节码进行修改,然后返回修改后的字节码。
- 类定义: JVM 使用修改后的字节码来定义类。
2.2、Instrumentation API
我这里使用的是JDK 21版本,Instrumentation类包含在java.instrument模块中。
package java.lang.instrument;
import java.security.ProtectionDomain;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarFile;
public interface Instrumentation {
/**
* JDK1.6引入的方法,注册一个ClassFileTransformer,用于在类加载时对字节码进行转换。
* 当类加载器加载某个类时,JVM 会调用注册的 ClassFileTransformer 的 transform 方法,允许我们对字节码进行修改。
*/
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
void addTransformer(ClassFileTransformer transformer);
boolean removeTransformer(ClassFileTransformer transformer);
boolean isRetransformClassesSupported();
/**
* 重新转换已经加载的类,允许我们对已经加载到 JVM 中的类进行再次转换,实现热部署等功能。
* 这个方法是JDK 1.6引入的,这个方法很经常会被使用到。
*/
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);
/**
* 在 Java 9 中,为了更好地支持模块化系统,Instrumentation 接口新增了一个重要方法:
* redefineModule。这个方法允许我们在运行时对已加载的模块进行重新定义。
*/
void redefineModule(Module module,
Set<Module> extraReads,
Map<String, Set<Module>> extraExports,
Map<String, Set<Module>> extraOpens,
Set<Class<?>> extraUses,
Map<Class<?>, List<Class<?>>> extraProvides);
boolean isModifiableModule(Module module);
}
3、加载Java Agent
大致了解了其实现原理后,我们来初步使用下。Java Instrumentation通常有2种方式可以加载Java Agent:
- 静态代理,在JVM启动时通过-javaagent 选项加载代理,适用于需要在应用启动阶段插入逻辑的场景,如性能监控工具。其实现原理:
- 静态代理需要在启动时定义一个包含 premain 方法的 Java Agent。
- JVM 在启动时会调用 premain 方法,并传入 Instrumentation 实例。
- 动态代理,在应用程序运行过程中,通过 Attach API 动态加载,适用于需要在运行时动态注入逻辑的场景,如调试工具或热部署。其实现原理:
- 动态代理使用 JVM 提供的 Attach API(com.sun.tools.attach 包)。
- 动态代理的 Agent 需要实现 agentmain 方法,JVM 会在 Attach 时调用它。
3.1、静态Agent示例
3.1.1、定义一个agent
package org.example.instrument.static_load;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class MyAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("---- begin load agent ----:Static Agent loaded with args: " + agentArgs);
// 添加 Transformer
inst.addTransformer(new ExampleTransformer());
}
// 定义一个字节码转换器
static class ExampleTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("org/example/instrument/static_load/MyDemo")) {
System.out.println("---- Transforming class: " + className);
// 这里可以修改字节码,示例中直接返回原字节码
return classfileBuffer;
}
return classfileBuffer;
}
}
}
3.1.2、配置 MANIFEST.MF
集成maven,我这里使用的是maven3.9.9版本。其中maven-jar-plugin使用的是3.4.2版本,注意maven插件不同,配置属性项也不同,需要根据版本按需修改。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>org.example.instrument.static_load.MyAgent</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
3.1.3、定义main测试类
public static void main(String[] args) {
System.out.println("hello static agent");
}
3.1.4、启动参数添加-javaagent
-javaagent:E:\idea_projects\java-agent-demo\target\java-agent-demo-1.0-SNAPSHOT.jar=agentArgs1
这里的java-agent-demo-1.0-SNAPSHOT.jar是上面maven package出来的包路径和名称,这里没有做修改直接拿来测试使用,agentArgs1为传入agent的参数。
运行效果如下:
3.2、动态Agent示例
3.2.1、定义一个动态 Agent
和静态agent大同小异,只是入口方法premain改为了agentmain。
package org.example.instrument.dynamic_load;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class MyDynamicAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("---- begin load agent ----:Static Agent loaded with args: " + agentArgs);
// 添加 Transformer
inst.addTransformer(new ExampleTransformer(), true);
}
// 定义一个字节码转换器
static class ExampleTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("org/example/instrument/dynamic_load/MyDynamicDemo")) {
System.out.println("---- Transforming class: " + className);
// 这里可以修改字节码,示例中直接返回原字节码
return classfileBuffer;
}
return classfileBuffer;
}
}
}
3.2.2、配置 MANIFEST.MF
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>org.example.instrument.dynamic_load.MyDynamicAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
3.2.3、定义main测试类
package org.example.instrument.dynamic_load;
import com.sun.tools.attach.VirtualMachine;
public class MyDynamicDemo {
public static void main(String[] args) {
try {
// 目标 JVM 的进程 ID(使用 jps 命令获取)。这里812进程号,是我另起了一个java进程的id
String targetJvmPid = "812";
// Attach 到目标 JVM
VirtualMachine vm = VirtualMachine.attach(targetJvmPid);
// 加载动态 Agent
vm.loadAgent("E:\\idea_projects\\java-agent-demo\\target\\java-agent-demo-1.0-SNAPSHOT.jar", "dynamicAgentArgs1231");
// Detach
vm.detach();
System.out.println("--- Agent attached successfully.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
3.2.4、运行结果
直接运行MyDynamicDemo类,由于这里使用API,动态的将agent附加到另一个JVM进程上,因此无需像静态agent一样指定-javaagent参数。
agent附加成功:
目标JVM上打印:
3.2.5、总结遇到的问题
由于我这里使用的是JDK21版本,21版本已经将jdk.attach做为单独模块,因此开始很容易遇到找不到jdk.attach包。需要绑定jdk.attach模块。
4、总结
至此,我们已经初步认识了 Instrumentation,他是 JVM 提供的一个强大工具,通过它可以实现动态字节码修改、性能监控、热部署等功能。通过合理使用 Instrumentation,开发者可以大幅提升系统的动态性与可维护性。