一、Instrumentation 是什么
Instrumentation 是 Java SE 5 引入的一套 JVM 级别的字节码增强机制,它允许开发者在类加载时期对字节码进行修改,而无需改动原始源代码。这套机制的核心接口定义在 java.lang.instrument 包中,通过 JVMTI(JVM Tool Interface)来与 JVM 底层交互。
在 Android 平台上,Instrumentation 的概念被进一步扩展。ART(Android Runtime)从 Android 7.0(API 24)开始支持 JVMTI(最初称为 OpenJDK JVMTI),并在 Android 8.0(API 26)之后通过 android.app.Instrumentation 和 debuggable 标志来允许 profiling 工具附加到运行时。Android 9 (API 28) 开始,JVMTI 能力得到大幅提升,通过 wrap.sh 机制甚至可以加载 agent 到 release 版本的 APK(虽然权限受限)。
Instrumentation 的核心价值在于:
- 非侵入性:不需要修改源代码,不依赖编译器的特殊处理。
- 全量覆盖:所有被类加载器加载的类都可以被拦截和修改。
- 运行时灵活性:
retransformClasses和redefineClasses支持在运行时重新转换已加载的类。 - 性能分析基础:几乎所有 JVM 生态的 APM 工具(如 SkyWalking、Pinpoint、Arthas)都基于 Instrumentation 机制。
二、Instrumentation 的核心 API
2.1 java.lang.instrument 包结构
该包包含以下核心类型:
| 类/接口 | 作用 |
|---|---|
java.lang.instrument.Instrumentation |
核心接口,提供 transform、redefine、retransform 方法 |
java.lang.instrument.ClassFileTransformer |
类文件转换器接口,实现 transform 方法 |
java.lang.instrument.ClassDefinition |
封装一个类的重定义(类名 + 新字节码) |
java.lang.instrument.IllegalClassFormatException |
当 transform 返回非法的类文件格式时抛出 |
java.lang.instrument.UnmodifiableClassException |
对不可修改的类调用 retransformClasses 时抛出 |
2.2 Instrumentation 接口的核心方法
public interface Instrumentation { |
2.3 ClassFileTransformer 接口
这是开发者实现字节码转换的核心接口,只有一个方法:
public interface ClassFileTransformer { |
当 JVM 加载一个类时,会依次调用所有已注册的 ClassFileTransformer:
- 传入参数
classfileBuffer是上一个 transformer 处理后的字节码 - 返回值为处理后的字节码(如果返回 null,表示不修改这个类)
classBeingRedefined在 retransform/redefine 场景下会指向正在被重定义的 Class 对象
注意:transform 方法必须保证线程安全,因为它会被多个类加载线程并发调用。
三、Java Agent 启动机制
3.1 premain:启动时 agent
在 JVM 启动时,通过 -javaagent 参数指定的 agent jar 会在 main 方法执行前被加载。agent jar 的 MANIFEST.MF 必须指定 Premain-Class:
Manifest-Version: 1.0 |
premain 方法的签名有两种形式:
// 形式一:基础版本 |
JVM 的启动流程中,agent 加载发生在类加载系统的初始化阶段。以 HotSpot JVM 为例,源码路径 src/hotspot/share/prims/jvmtiEnv.cpp 中的 JvmtiEnv::SetEventCallbacks 和 JvmtiEnv::AddToSystemClassLoaderSearch 是关键的 native 入口。JVM 在 Threads::create_vm 中会调用 JvmtiExport::load_agent_library 来加载 -javaagent 指定的所有 agent。
3.2 agentmain:运行时附加 agent
Instrumentation 还支持在 JVM 运行过程中动态附加 agent,这依赖 Attach API(com.sun.tools.attach):
// 获取当前 JVM 的所有虚拟机的 pid |
agent jar 的 MANIFEST.MF 需要指定 Agent-Class 而非 Premain-Class:
Manifest-Version: 1.0 |
对应的入口方法签名:
public static void agentmain(String agentArgs, Instrumentation inst); |
3.3 HotSpot 中 agent 加载的底层流程
在 HotSpot JVM 中,-javaagent 的加载链路如下:
java.c中的JLI_Launch→ParseArguments解析命令行参数,识别-javaagentArguments::add_init_agent将 agent 路径和参数存入_agentListThreads::create_vm→JvmtiExport::load_agent_library遍历_agentList- 通过
os::dll_load加载 agent 的 .jar 文件(实际先解压 .so / .dll) - 查找
Agent_OnLoad函数(C 语言入口),或通过JPLISAgent支持 Java agent - Java agent 最终通过
sun.instrument.InstrumentationImpl的 native 方法注册到 JVMTI
具体的 Java 侧实现位于 JDK 源码的 src/java.instrument/share/classes/sun/instrument/InstrumentationImpl.java,该类的 native 方法对应 HotSpot 的 src/java.instrument/share/native/libinstrument/InstrumentationImplNativeMethods.c。
四、retransformClasses 与 redefineClasses 的区别
这是面试中的高频考点。两者的核心区别在于对类结构的修改能力:
4.1 redefineClasses
void redefineClasses(ClassDefinition... definitions) |
能力边界:
- 可以增加、删除、修改方法体
- 可以增加、删除字段
- 可以修改方法的访问修饰符
- 可以修改类的注解
- 不可以修改类的继承关系(父类、接口列表)
- 不可以修改类的签名(类名)
实现机制:在 JVM 内部,redefine 实际上是一次”类替换”。HotSpot 的实现位于 src/hotspot/share/prims/jvmtiRedefineClasses.cpp,核心函数是 VM_RedefineClasses::doit()。它会创建一个新的 InstanceKlass 对象来替换旧的,然后将旧对象标记为 “obsolete”。旧类的已存在实例仍然可以使用旧的方法版本(通过 “EMCP” - Equivalent Method Constant Pool 机制),但新调用会使用新方法。
4.2 retransformClasses
void retransformClasses(Class<?>... classes) |
能力边界:
- 只能修改方法体(即 Code 属性)
- 不能增加或删除方法
- 不能增加或删除字段
- 不能修改类的继承关系
- 不能修改属性、签名等元数据
实现机制:retransformClasses 的前提是 addTransformer 时传入了 canRetransform=true。JVM 会保留原始的类文件字节码(在 InstanceKlass 的 _cached_class_file 字段中),当 retransform 被触发时,JVM 从缓存中取出原始字节码,再次经过所有 canRetransform=true 的 transformer 处理。
在 HotSpot 中,VM_RetransformClasses::doit() 执行 retransform 操作,其核心逻辑是调用 InstanceKlass::set_cached_class_file() 和 JvmtiClassFileLoadEventPoster 来重新触发 ClassFileLoadHook 事件。
4.3 使用场景对比
| 场景 | 推荐使用 |
|---|---|
| APM 监控(方法耗时统计) | retransformClasses |
| 热修复(修改方法逻辑) | retransformClasses |
| AOP 切面编织 | addTransformer (在类加载时) |
| 动态增加字段 | redefineClasses |
| 动态添加接口实现 | redefineClasses |
| 类结构重构 | redefineClasses |
五、ART 中的 Instrumentation 支持
5.1 Android ART 的 JVMTI 支持
Android 8.0 (Oreo, API 26) 开始,ART 引入了对 OpenJDK JVMTI 的支持。相关代码位于 AOSP 的 art/openjdkjvmti/ 目录下:
art/openjdkjvmti/ti_class.cc:实现RetransformClasses、RedefineClasses等 JVMTI 类操作art/openjdkjvmti/ti_redefine.cc:类的重定义具体实现art/openjdkjvmti/ti_breakpoint.cc:断点相关实现art/openjdkjvmti/ti_method.cc:方法级别的 JVMTI 操作
在 ART 中,retransformClasses 的实现与 dex2oat 紧密相关。ART 使用 RetransformClasses 调用时,会在 art/runtime/jit/jit.cc 中触发 JIT 重新编译:
// art/openjdkjvmti/ti_redefine.cc |
5.2 Android Profiler 的实现原理
Android Studio 中的 CPU Profiler 和 Memory Profiler 其底层正是通过 JVMTI 与 ART 通信。核心流程:
adb shell am attach-agent <pid> /data/local/tmp/perfa.jar—— 将 profiler agent 附加到目标进程- Agent 通过 JVMTI 的
SetEventCallbacks注册事件监听 - 对于方法采样:使用
SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, ...)开启方法进入事件 - 所有方法进入事件通过 Binder 或 socket 发送到 Android Studio
5.3 D8/R8 对 Instrumentation 的影响
Android 构建工具链中的 D8(desugar)和 R8(shrinking/optimization)会对字节码做大幅度修改。在使用 Instrumentation 时需要注意:
- D8 会将 Java 8 的 lambda 表达式 desugar 为匿名内部类
- R8 会内联方法、删除未使用的代码、混淆类名
- 因此 Instrumentation 的 transform 规则需要考虑这些修改后的类结构
建议在 Android 开发中使用 -keep 规则保留关键的类和方法,避免 R8 的优化破坏 Instrumentation 的语义。
六、实战:编写一个方法耗时统计 Agent
6.1 项目结构
method-profiler-agent/ |
6.2 Agent 入口
package com.example.agent; |
6.3 ClassFileTransformer 实现
这里我们使用 ASM 来操作字节码(你也可以选择 Javassist 或 ByteBuddy):
package com.example.agent; |
6.4 使用 ASM 植入耗时统计代码
class ProfilingClassVisitor extends ClassVisitor { |
6.5 打包与使用
Maven 配置 (pom.xml):
<build> |
使用方式:
# 方式一:启动时指定 agent |
6.6 Android 平台的 Agent 使用
在 Android 上,使用 agent 的方式略有不同。Android 9+ 支持通过 wrap.sh 来加载 agent:
# 创建 wrap.sh |
更常见的方式是通过 Android Studio Profiler 或使用 run-as + cmd activity 命令来配合 JVMTI agent 进行调试。
七、Instrumentation 与其他字节码增强方案的对比
7.1 Instrumentation vs ASM
| 维度 | Instrumentation | ASM |
|---|---|---|
| 层次 | JVM 内置 API,在类加载时拦截 | 字节码操作框架,直接读写 .class 文件 |
| 使用方式 | 通过 agent 注册 transformer | 直接操作 ClassReader/ClassWriter |
| 时机 | 类加载时或 retransform 时 | 编译后、加载前(Gradle plugin) |
| 运行时修改 | 支持(retransform/redefine) | 不支持运行时修改 |
| 学习曲线 | 中 | 高(需深入理解字节码结构) |
| 典型用途 | APM、热修复、调试器 | AOP 框架(Spring AOP)、代码生成 |
实际上,两者是互补关系:ASM 常常被用作 Instrumentation agent 中的字节码操作工具。例如,在 ClassFileTransformer.transform() 方法中,你收到的是原始字节码(byte[]),此时可以使用 ASM 来解析、修改并生成新的字节码。
7.2 Instrumentation vs AspectJ
| 维度 | Instrumentation | AspectJ |
|---|---|---|
| 织入时机 | 类加载时(load-time weaving) | 编译时(compile-time weaving)或加载时 |
| 抽象层次 | 字节码级别,需手动操作 | 切面语言(pointcut + advice),高层抽象 |
| 侵入性 | 零侵入(不修改源码) | 需引入 AspectJ 注解或语法 |
| 性能 | 取决于 transformer 的效率 | 静态织入性能好,动态织入有开销 |
| Android 支持 | 有限(需 root/debuggable) | 不支持(AspectJ 织入器针对 JVM,非 ART) |
7.3 选型建议
- 编译期代码生成:选择 JSR-269 APT(如 ButterKnife、Dagger、Room)
- 编译后字节码修改:选择 ASM + Gradle Transform(Android)/ ASM + Maven Plugin(Java)
- 运行时监控/APM:选择 Instrumentation + ASM/Javassist
- 运行时热修复:选择 Instrumentation 的 retransformClasses 或 Android 的 Tinker/Sophix
- AOP 编程:Spring 项目选择 AspectJ 或 Spring AOP;Android 项目选择 ASM + Gradle Transform
八、Instrumentation 的局限性
8.1 类加载顺序问题
premain 的 transformer 在 main 方法之前注册,但它不能拦截到 Bootstrap ClassLoader 加载的基础类(如 java.lang.String、java.lang.Object)——这些类在 agent 注册之前就已经加载完成。要修改这些类,你需要使用 Boot-Class-Path MANIFEST 属性将 agent 类加入 Bootstrap ClassLoader 的搜索路径,或者使用 retransformClasses 对它们进行重转换。
8.2 类加载器隔离
在复杂容器(如 Tomcat、OSGi)中,多个 ClassLoader 可能会加载同名的类。transform 方法的 className 参数使用的是内部名称(如 java/lang/String),无法直接区分是哪个 ClassLoader 的副本。不过 loader 参数提供了 ClassLoader 引用,可以用来做进一步的过滤。
8.3 循环引用问题
Agent 自身的类(以及其依赖的类,如 ASM)在被加载时也会触发 transform。如果不做过滤,可能会导致无限循环。解决方案:
- 在 transform 方法开头判断类名,排除 agent 自身所在的包
- 使用自定义 ClassLoader 加载 agent 相关类
if (className.startsWith("com/example/agent/")) { |
8.4 Android 平台的限制
在 Android 上使用 Instrumentation 面临额外的限制:
- 普通应用(非 debuggable、非 root)无法使用 JVMTI
retransformClasses需要 ART 的 JIT 编译器支持,部分设备可能不支持- Android 的 APK 打包格式(DEX)与 JVM 的 .class 格式不同,需要先经过转换
- 系统分区(/system)中的应用受到 dm-verity 保护,不能进行 retransform
九、在 AOSP 中 Instrumentation 相关的源码路径
以下是 Android AOSP 中与 Instrumentation 和 JVMTI 相关的重要源码文件:
| 源码文件 | 作用 |
|---|---|
art/openjdkjvmti/ti_class.cc |
JVMTI 类操作(包括 retransform/redefine)的实现 |
art/openjdkjvmti/ti_redefine.cc |
类重定义的核心逻辑 |
art/openjdkjvmti/ti_transformation.cc |
Transformation 事件的处理 |
art/openjdkjvmti/ti_breakpoint.cc |
断点设置(也是 Instrumentation 的一部分) |
art/openjdkjvmti/OpenjdkJvmTi.cc |
JVMTI 环境的初始化 |
art/openjdkjvmti/events.cc |
JVMTI 事件分发 |
art/runtime/jit/jit.cc |
JIT 编译管理,与 retransform 后的重编译相关 |
art/runtime/class_linker.cc |
类链接器,负责类的加载和链接 |
art/runtime/instrumentation.cc |
ART 自有的 Instrumentation 支持(方法进入/退出事件) |
libcore/dalvik/src/main/java/dalvik/system/Instrumentation.java |
Android Framework 层的 Instrumentation 封装 |
十、常见面试题
Q1: ClassFileTransformer 的 transform 方法会在什么时候被调用?
A: transform 方法在两个时机被调用:(1) 当 JVM 加载一个新类时,在类定义(defineClass)之前,所有已注册的 transformer 会被依次调用。(2) 当调用 retransformClasses() 或 redefineClasses() 时,JVM 会重新触发对已加载类的 transform。在第一种情况下,classBeingRedefined 参数为 null;在第二种情况下,该参数指向正在被重定义的 Class 对象。注意在 transform 方法内部不能对正在被转换的类进行反射操作,否则可能导致死锁或类加载循环。
Q2: retransformClasses 和 redefineClasses 的本质区别是什么?
A: 本质区别有两点。(1) 能力范围:retransform 只能修改方法体(即 Code 属性),而 redefine 可以修改类的完整结构,包括增删字段、方法、修改访问修饰符等。(2) 原始字节码的保留:retransform 始终保留原始的类文件字节码(存在 InstanceKlass::_cached_class_file 中),每次 retransform 都是从原始字节码开始再经过所有 transformer;而 redefine 是直接的类替换,被替换后的类的新”原始”版本就是替换时的定义。这意味着多次 retransform 不会叠加,而多次 redefine 会叠加。另外,redefine 会修改常量池,retransform 则不会。
Q3: 在 Android 上,如何实现对 release 应用的运行时监控?
A: 在 Android 上对 release 版本进行 Instrumentation 是非常困难的,主要是因为:(1) release APK 的 android:debuggable 为 false,JVMTI 无法附加。(2) ART 对 debuggable=false 的进程会做更多优化(如 AOT 编译),减少 JIT 的使用。(3) Android 的安全模型(SELinux、seccomp)限制了 ptrace 和 /proc/self/mem 等常规 Linux 调试手段。可行的方案包括:(a) 使用 root 权限的设备,通过 wrap.sh 在进程启动时加载 agent;(b) 在编译阶段通过 Gradle Transform API 植入监控代码;(c) 使用 Xposed / Frida 等 hook 框架进行运行时注入(此方案有合规风险)。对于大多数商业场景,推荐使用编译期字节码增强方案(ASM + Gradle Plugin)而非运行时 Instrumentation。
Q4: addTransformer(transformer, true) 中的第二个参数有什么作用?
A: 第二个参数 canRetransform 决定该 transformer 是否能够参与 retransformClasses 触发的转换。如果设为 false(默认),该 transformer 只在类首次加载时被调用,在调用 retransformClasses 时不会触发它。如果设为 true,该 transformer 在类加载时和 retransform 时都会被调用。在 APM 场景下通常需要设为 true,因为已加载的类需要通过 retransformClasses 来再次触发 transform。但需要注意:设置为 true 的 transformer 必须能够正确处理两次(甚至多次)调用同一个类的情况,通常在 transform 方法中通过判断类是否已被处理过来避免重复织入。
Q5: 为什么 transform 方法不能对同一个类多次织入监控代码?
A: 如果 transform 方法没有做幂等性判断,每次 retransform 都会向方法体中追加一段监控代码。这会导致:(1) 方法体越来越大,执行效率下降;(2) 监控数据重复上报(每次方法调用被记录多次);(3) 最终可能超出方法的最大字节码长度限制(65535 字节)。解决方案:在 transform 时检查类的某个标记(如新增一个特定的注解或接口,或检查类名/方法名是否已经包含特定的前缀),如果发现类已经被增强过,则跳过织入。
Q6: premain 和 agentmain 中获取的 Instrumentation 实例有什么区别吗?
A: 没有区别。无论是通过 premain 还是 agentmain 获取的 Instrumentation 实例,其底层都是同一个 JVMTI 环境的代理。区别在于初始的 JVMTI 环境 capability 设置:在 premain 阶段(OnLoad phase),JVMTI 可以添加 Bootstrap ClassLoader 搜索路径,可以设置 Native Method Prefix——这些操作在 Live phase(agentmain 阶段)是不允许的。因此 premain 拥有更全面的能力,而 agentmain 主要用于运行时监控和诊断。
参考文档:
- Java SE Instrumentation Package: https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html
- JVM Tool Interface (JVM TI) Specification
- AOSP:
art/openjdkjvmti/目录下的 JVMTI 实现 - AOSP:
art/runtime/instrumentation.cc— ART 自有的 Instrumentation 机制





