目录
  1. 1. 一、Instrumentation 是什么
  2. 2. 二、Instrumentation 的核心 API
    1. 2.1. 2.1 java.lang.instrument 包结构
    2. 2.2. 2.2 Instrumentation 接口的核心方法
    3. 2.3. 2.3 ClassFileTransformer 接口
  3. 3. 三、Java Agent 启动机制
    1. 3.1. 3.1 premain:启动时 agent
    2. 3.2. 3.2 agentmain:运行时附加 agent
    3. 3.3. 3.3 HotSpot 中 agent 加载的底层流程
  4. 4. 四、retransformClasses 与 redefineClasses 的区别
    1. 4.1. 4.1 redefineClasses
    2. 4.2. 4.2 retransformClasses
    3. 4.3. 4.3 使用场景对比
  5. 5. 五、ART 中的 Instrumentation 支持
    1. 5.1. 5.1 Android ART 的 JVMTI 支持
    2. 5.2. 5.2 Android Profiler 的实现原理
    3. 5.3. 5.3 D8/R8 对 Instrumentation 的影响
  6. 6. 六、实战:编写一个方法耗时统计 Agent
    1. 6.1. 6.1 项目结构
    2. 6.2. 6.2 Agent 入口
    3. 6.3. 6.3 ClassFileTransformer 实现
    4. 6.4. 6.4 使用 ASM 植入耗时统计代码
    5. 6.5. 6.5 打包与使用
    6. 6.6. 6.6 Android 平台的 Agent 使用
  7. 7. 七、Instrumentation 与其他字节码增强方案的对比
    1. 7.1. 7.1 Instrumentation vs ASM
    2. 7.2. 7.2 Instrumentation vs AspectJ
    3. 7.3. 7.3 选型建议
  8. 8. 八、Instrumentation 的局限性
    1. 8.1. 8.1 类加载顺序问题
    2. 8.2. 8.2 类加载器隔离
    3. 8.3. 8.3 循环引用问题
    4. 8.4. 8.4 Android 平台的限制
  9. 9. 九、在 AOSP 中 Instrumentation 相关的源码路径
  10. 10. 十、常见面试题
【深入理解JVM字节码】第十篇、Java Instrumentation原理

一、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.Instrumentationdebuggable 标志来允许 profiling 工具附加到运行时。Android 9 (API 28) 开始,JVMTI 能力得到大幅提升,通过 wrap.sh 机制甚至可以加载 agent 到 release 版本的 APK(虽然权限受限)。

Instrumentation 的核心价值在于:

  1. 非侵入性:不需要修改源代码,不依赖编译器的特殊处理。
  2. 全量覆盖:所有被类加载器加载的类都可以被拦截和修改。
  3. 运行时灵活性retransformClassesredefineClasses 支持在运行时重新转换已加载的类。
  4. 性能分析基础:几乎所有 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 {
// 添加一个 ClassFileTransformer(启动时通过 premain 指定)
void addTransformer(ClassFileTransformer transformer, boolean canRetransform);

// 移除一个 ClassFileTransformer
boolean removeTransformer(ClassFileTransformer transformer);

// 重新转换已加载的类——不会改变类的主体结构(字段数、方法数不变)
void retransformClasses(Class<?>... classes)
throws UnmodifiableClassException;

// 重新定义类——可以改变类的主体结构(增删字段/方法),但不能改变类的 schema
void redefineClasses(ClassDefinition... definitions)
throws ClassNotFoundException, UnmodifiableClassException;

// 获取所有已加载的类
Class[] getAllLoadedClasses();

// 获取所有已经被 transform 的类
Class[] getInitiatedClasses(ClassLoader loader);

// 获取对象大小
long getObjectSize(Object objectToSize);

// 判断当前 JVM 是否支持 retransform
boolean isRetransformClassesSupported();

// 判断当前 JVM 是否支持 redefine
boolean isRedefineClassesSupported();

// 是否原生方法前缀支持
boolean isNativeMethodPrefixSupported();

// 设置原生方法前缀(用于 native 方法的拦截)
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

2.3 ClassFileTransformer 接口

这是开发者实现字节码转换的核心接口,只有一个方法:

public interface ClassFileTransformer {
byte[] transform(
ClassLoader loader,
String className, // 类的内部名称,如 java/lang/String
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer // 原始字节码
) throws IllegalClassFormatException;
}

当 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-Class: com.example.MyAgent
Can-Retransform-Classes: true
Can-Redefine-Classes: true

premain 方法的签名有两种形式:

// 形式一:基础版本
public static void premain(String agentArgs, Instrumentation inst);

// 形式二:无参版本(如果形式一不存在,JVM会尝试调用此版本)
public static void premain(String agentArgs);

JVM 的启动流程中,agent 加载发生在类加载系统的初始化阶段。以 HotSpot JVM 为例,源码路径 src/hotspot/share/prims/jvmtiEnv.cpp 中的 JvmtiEnv::SetEventCallbacksJvmtiEnv::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
List<VirtualMachineDescriptor> vms = VirtualMachine.list();
// 选择目标 VM
VirtualMachineDescriptor target = vms.get(0);
// 附加到目标 VM
VirtualMachine vm = VirtualMachine.attach(target.id());
// 加载 agent jar
vm.loadAgent("/path/to/agent.jar", "agent-arguments");
vm.detach();

agent jar 的 MANIFEST.MF 需要指定 Agent-Class 而非 Premain-Class

Manifest-Version: 1.0
Agent-Class: com.example.MyAgent
Can-Retransform-Classes: true

对应的入口方法签名:

public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs);

3.3 HotSpot 中 agent 加载的底层流程

在 HotSpot JVM 中,-javaagent 的加载链路如下:

  1. java.c 中的 JLI_LaunchParseArguments 解析命令行参数,识别 -javaagent
  2. Arguments::add_init_agent 将 agent 路径和参数存入 _agentList
  3. Threads::create_vmJvmtiExport::load_agent_library 遍历 _agentList
  4. 通过 os::dll_load 加载 agent 的 .jar 文件(实际先解压 .so / .dll)
  5. 查找 Agent_OnLoad 函数(C 语言入口),或通过 JPLISAgent 支持 Java agent
  6. 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)
throws ClassNotFoundException, UnmodifiableClassException;

能力边界

  • 可以增加、删除、修改方法体
  • 可以增加、删除字段
  • 可以修改方法的访问修饰符
  • 可以修改类的注解
  • 不可以修改类的继承关系(父类、接口列表)
  • 不可以修改类的签名(类名)

实现机制:在 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)
throws UnmodifiableClassException;

能力边界

  • 只能修改方法体(即 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:实现 RetransformClassesRedefineClasses 等 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
jvmtiError Redefiner::RedefineClasses(jvmtiEnv* env,
jint class_count,
const jvmtiClassDefinition* definitions) {
// 1. 验证输入
// 2. 为每个类创建新的 ClassDef
// 3. 调用 ClassLinker::UpdateClass 更新类定义
// 4. 触发 JIT 重编译
// 5. 通知 GC 更新根引用
}

5.2 Android Profiler 的实现原理

Android Studio 中的 CPU Profiler 和 Memory Profiler 其底层正是通过 JVMTI 与 ART 通信。核心流程:

  1. adb shell am attach-agent <pid> /data/local/tmp/perfa.jar —— 将 profiler agent 附加到目标进程
  2. Agent 通过 JVMTI 的 SetEventCallbacks 注册事件监听
  3. 对于方法采样:使用 SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, ...) 开启方法进入事件
  4. 所有方法进入事件通过 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/
├── pom.xml
└── src/main/java/com/example/agent/
├── MethodProfilerAgent.java
├── MethodProfilerTransformer.java
└── MethodProfilerAdvice.java

6.2 Agent 入口

package com.example.agent;

import java.lang.instrument.Instrumentation;

public class MethodProfilerAgent {

// premain: 通过 -javaagent 启动时调用
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("[Profiler] premain called, args: " + agentArgs);
inst.addTransformer(new MethodProfilerTransformer(), true);
}

// agentmain: 运行时 attach 时调用
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("[Profiler] agentmain called, args: " + agentArgs);
inst.addTransformer(new MethodProfilerTransformer(), true);

// 对已加载的类进行 retransform
Class<?>[] loadedClasses = inst.getAllLoadedClasses();
for (Class<?> clazz : loadedClasses) {
// 过滤:只能对可修改的类进行 retransform
if (inst.isModifiableClass(clazz)
&& clazz.getName().startsWith("com.example")) {
try {
inst.retransformClasses(clazz);
} catch (Exception e) {
System.err.println("Failed to retransform: " + clazz.getName());
}
}
}
}
}

6.3 ClassFileTransformer 实现

这里我们使用 ASM 来操作字节码(你也可以选择 Javassist 或 ByteBuddy):

package com.example.agent;

import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MethodProfilerTransformer implements ClassFileTransformer {

@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {

// 排除 JDK 内部类、ASM 自身的类、以及 lambda 生成的类
if (className == null
|| className.startsWith("java/")
|| className.startsWith("jdk/")
|| className.startsWith("sun/")
|| className.startsWith("org/objectweb/asm/")
|| className.startsWith("com/example/agent/")
|| className.contains("$$Lambda$")) {
return null;
}

try {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(
cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new ProfilingClassVisitor(
Opcodes.ASM9, cw, className.replace('/', '.'));
cr.accept(cv, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
} catch (Exception e) {
System.err.println("Failed to transform: " + className);
e.printStackTrace();
return null; // 返回 null 表示不做修改
}
}
}

6.4 使用 ASM 植入耗时统计代码

class ProfilingClassVisitor extends ClassVisitor {
private final String className;

public ProfilingClassVisitor(int api, ClassVisitor cv, String className) {
super(api, cv);
this.className = className;
}

@Override
public MethodVisitor visitMethod(int access, String name,
String descriptor,
String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name,
descriptor, signature, exceptions);
// 跳过构造方法 <init> 和静态初始化 <clinit>
if (name.equals("<init>") || name.equals("<clinit>")) {
return mv;
}
// 跳过抽象方法和 native 方法
if ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != 0) {
return mv;
}

return new ProfilingMethodVisitor(Opcodes.ASM9, mv,
access, name, descriptor, className);
}
}

class ProfilingMethodVisitor extends AdviceAdapter {
private final String className;
private final String methodName;
private int startTimeVar;

protected ProfilingMethodVisitor(int api, MethodVisitor mv,
int access, String name,
String desc, String className) {
super(api, mv, access, name, desc);
this.className = className;
this.methodName = name;
}

@Override
protected void onMethodEnter() {
// long startTime = System.nanoTime();
startTimeVar = newLocal(Type.LONG_TYPE);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"nanoTime", "()J", false);
mv.visitVarInsn(LSTORE, startTimeVar);
}

@Override
protected void onMethodExit(int opcode) {
// long cost = System.nanoTime() - startTime;
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"nanoTime", "()J", false);
mv.visitVarInsn(LLOAD, startTimeVar);
mv.visitInsn(LSUB);
int costVar = newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, costVar);

// 打印耗时:System.out.println(className.methodName cost: " + cost + "ns");
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out",
"Ljava/io/PrintStream;");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder",
"<init>", "()V", false);
mv.visitLdcInsn(className + "." + methodName + " cost: ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder",
"append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(LLOAD, costVar);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder",
"append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn("ns");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder",
"append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder",
"toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream",
"println", "(Ljava/lang/String;)V", false);
}
}

6.5 打包与使用

Maven 配置 (pom.xml):

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.example.agent.MethodProfilerAgent</Premain-Class>
<Agent-Class>com.example.agent.MethodProfilerAgent</Agent-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<!-- 将 ASM 依赖打入 jar -->
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

使用方式:

# 方式一:启动时指定 agent
java -javaagent:method-profiler-agent.jar=com.example.MyClass \
-jar myapp.jar

# 方式二:运行时 attach(需要目标 JVM 的 pid)
# 通过 Attach API 或 jcmd 命令

6.6 Android 平台的 Agent 使用

在 Android 上,使用 agent 的方式略有不同。Android 9+ 支持通过 wrap.sh 来加载 agent:

# 创建 wrap.sh
#!/system/bin/sh
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/data/local/tmp
exec /system/bin/app_process -Xbootclasspath/a:/data/local/tmp/agent.jar \
/system/bin --nice-name=com.example.myapp com.example.myapp

更常见的方式是通过 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.Stringjava.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/")) {
return null; // 跳过 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 主要用于运行时监控和诊断。


参考文档:

打赏
  • 微信
  • 支付宝

评论