目录
  1. 1. 一、反射与字节码:两种调用路径
  2. 2. 二、Class.forName 的完整加载链路
    1. 2.1. 2.1 三层调用架构
    2. 2.2. 2.2 ClassLinker 的核心角色
    3. 2.3. 2.3 Class.forName vs ClassLoader.loadClass
    4. 2.4. 2.4 DexFile.FindClassDef:DEX 文件中的类查找
  3. 3. 三、Method.invoke 的完整调用流程
    1. 3.1. 3.1 Java 层入口
    2. 3.2. 3.2 Native 层:InvokeMethod
    3. 3.3. 3.3 ArtMethod::Invoke 的执行
    4. 3.4. 3.4 FromReflectedMethod 的实现
  4. 4. 四、反射性能开销的根源
    1. 4.1. 4.1 开销分解
    2. 4.2. 4.2 各场景的性能数据(近似)
  5. 5. 五、反射膨胀(Reflection Inflation)优化
    1. 5.1. 5.1 工作原理
    2. 5.2. 5.2 HotSpot 中的实现对比
    3. 5.3. 5.3 Inflation 的限制
  6. 6. 六、AccessibleObject.setAccessible 的机制
    1. 6.1. 6.1 实现原理
    2. 6.2. 6.2 安全限制
  7. 7. 七、MethodHandle 和 VarHandle:反射的演进
    1. 7.1. 7.1 MethodHandle(Java 7+)
    2. 7.2. 7.2 VarHandle(Java 9+)
    3. 7.3. 7.3 三者的性能对比
  8. 8. 八、动态代理(Dynamic Proxy)
    1. 8.1. 8.1 代理类生成的字节码
    2. 8.2. 8.2 代理类的内部结构
  9. 9. 九、Field 反射读写与 Constructor.newInstance
    1. 9.1. 9.1 Field.get/set 的实现
    2. 9.2. 9.2 Constructor.newInstance
  10. 10. 十、JIT Profiling 与 Inflation 触发机制
    1. 10.1. 10.1 计数器的维护
    2. 10.2. 10.2 dex2oat 的 AOT 预编译
  11. 11. 十一、反射对 GC 和内存的影响
  12. 12. 十二、Array 反射操作
  13. 13. 十三、Constructor 的反射与对象创建
  14. 14. 十四、实战建议与常见陷阱
    1. 14.1. 9.1 正确的反射使用模式
    2. 14.2. 9.2 常见陷阱
  15. 15. 十、AOSP 关键源码路径总结
  16. 16. 面试问答
【深入理解JVM字节码】第六篇、反射实现原理

一、反射与字节码:两种调用路径

在进行深入分析之前,先用字节码对比直接调用与反射调用的本质差异:

直接调用

aload_0
invokevirtual #5 // Method foo:()V

仅两条指令(3 字节字节码)。invokevirtual #5 使用常量池索引 #5 的符号引用,可在编译期确定目标方法。首次执行时 JVM 解析常量池,将符号引用替换为直接引用(vtable 索引或 ArtMethod*),后续调用直接查表或跳转,开销极低。

反射调用(等价逻辑):

ldc           #5  // class Test(类名字符串常量)
ldc #6 // String "foo"(方法名字符串常量)
iconst_0
anewarray #7 // class Class(参数类型数组)
invokevirtual #8 // Method Class.getDeclaredMethod:(..)
astore 2
// 然后 Method.invoke(receiver, args[])
// 涉及:装箱、数组打包、JNI 调用、ArtMethod::Invoke

即使忽略 Method.invoke 的调用成本,仅获取 Method 对象的开销就远超直接调用。反射调用的每个环节——类名查找、方法名匹配、参数类型拆装箱——都需要在运行时进行,编译期无法做任何优化。

二、Class.forName 的完整加载链路

Class.forName(String className) 是反射的入口之一。它的调用链路从 Java 层穿透到 Native 层,最终由 ART 的 ClassLinker 完成类的定位、加载和链接。

2.1 三层调用架构

Layer 1: Java 层

libcore/ojluni/src/main/java/java/lang/Class.java

public static Class<?> forName(String className) throws ClassNotFoundException {
// 1. 获取调用者的 ClassLoader
ClassLoader caller = Reflection.getCallerClassLoader();
// 2. 调用 forName0 native 方法(实现在 art/runtime/native/java_lang_Class.cc)
return forName(className, true, caller);
}

public static Class<?> forName(String name, boolean initialize, ClassLoader loader) {
// 3. 如果 loader 为 null,使用 BootClassLoader
// 4. 调用 Class.classForName(name, initialize, loader)
// 5. 内部调用 VMClassLoader.loadClass 或通过 ClassLoader.loadClass 委派
}

关键参数 initialize=trueClass.forName 默认执行类的 <clinit> 静态初始化方法。这对于 JDBC 驱动加载等需要触发 static {} 块的场景至关重要。

Layer 2: ClassLoader 委派链

ClassLoader.loadClass(String name)
→ 检查是否已加载(findLoadedClass)
→ 委派给 parent.loadClass()
→ 如果 parent 返回 null(未找到):findClass()

在 Android 中,findClass 最终进入 BaseDexClassLoader

BaseDexClassLoader.findClass(name)
→ DexPathList.findClass(name)
→ 遍历 dexElements[](APK 中的多个 DEX 文件)
→ 每个 element: DexFile.loadClassBinaryName(name, definingContext)
→ 如果找到:defineClass(name, loader, ...)

Layer 3: Native 层 DefineClass

art/runtime/native/dalvik_system_DexFile.cc

static jclass DexFile_defineClassNative(JNIEnv* env, jclass, jstring javaName,
jobject javaLoader, ...) {
// 1. 将 java 类名(如 "com.example.Foo")转换为 descriptor("Lcom/example/Foo;")
const char* descriptor = ...;

// 2. 在 DEX 文件中查找类定义(基于 descriptor 在 type_ids 中二分搜索)
const DexFile::ClassDef* class_def = dex_file.FindClassDef(descriptor);
if (class_def == nullptr) {
return nullptr; // 类未找到
}

// 3. 调用 ClassLinker::DefineClass 在堆上创建 Class 对象
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
ObjPtr<mirror::Class> klass = class_linker->DefineClass(self, descriptor,
class_loader, *dex_file,
*class_def);
// 4. 返回 Java 层的 Class 对象
return soa.AddLocalReference<jclass>(klass);
}

2.2 ClassLinker 的核心角色

art/runtime/class_linker.cc 是整个类加载系统的中枢,包含四个递进的阶段:

阶段 1: FindClass —— 查找

ObjPtr<mirror::Class> ClassLinker::FindClass(Thread* self, const char* descriptor,
Handle<mirror::ClassLoader> class_loader) {
// 1. 在 ClassTable 中查找(已加载类的哈希表,key 是 descriptor)
ObjPtr<mirror::Class> klass = LookupClass(self, descriptor, class_loader);
if (klass != nullptr) {
return klass; // 已加载,直接返回
}

// 2. 委派给 ClassLoader 加载
// (遍历 ClassLoader 委托链,最终调用 DexFile_defineClassNative)
klass = LoadClass(self, descriptor, class_loader);

return klass;
}

ClassTable 是 ART 中的全局类注册表,以 descriptor 字符串为 key。LookupClass 在这个哈希表中 O(1) 查找已加载的类。这保证了同一个类不会被重复加载——即使多次调用 Class.forName("com.example.Foo"),只有第一次触发加载流程,后续直接从 ClassTable 返回。

阶段 2: DefineClass —— 定义

ObjPtr<mirror::Class> ClassLinker::DefineClass(Thread* self, const char* descriptor,
Handle<mirror::ClassLoader> class_loader,
const DexFile& dex_file,
const DexFile::ClassDef& class_def) {
// 1. 读取 DEX 文件的 class_def 项
// 2. 在堆上分配 mirror::Class 对象(大小取决于类的复杂程度)
// 3. 设置 access_flags、class_loader、dex_file 引用等基础字段
// 4. 将 Class 对象插入 ClassTable(加 class_load_lock_ 锁)
// 5. 读取 class_def 中的注释和静态字段初始值
}

class_load_lock_ 是一个互斥锁(Mutex),确保同一个类不会被多个线程并发加载——第二个线程会在锁上等待第一个线程完成加载后直接从 ClassTable 获取结果。这避免了经典的”双重加载”竞赛条件。

阶段 3: LinkClass —— 链接

bool ClassLinker::LinkClass(Thread* self, const char* descriptor,
Handle<mirror::Class> klass, ...) {
// 1. 确保父类和所有接口类已被加载(通过 LoadClass 递归)
// 2. 构建 vtable(虚方法表):复制父类 vtable,替换/追加子类方法
// 3. 构建 iftable(接口方法表):为每个实现的接口分配方法槽位
// 4. 计算字段偏移量(field offsets):子类字段在父类字段之后排列
// 5. 解析方法中引用的类和字段符号引用
// 6. 验证字节码(如果 class 文件版本需要 StackMapTable 验证)
}

vtable 构建的细节(在 LinkVirtualMethods 中):先复制父类 vtable 的所有条目,然后遍历子类的 virtual 方法。如果某个方法与 vtable 中已有的方法有相同的名称和描述符(即 override),则用子类的 ArtMethod* 替换对应槽位;如果是新方法,则追加到 vtable 末尾。

阶段 4: EnsureInitialized —— 初始化

bool ClassLinker::EnsureInitialized(Thread* self, Handle<mirror::Class> klass, ...) {
// 如果类已经初始化(初始化状态标记为 kInitialized),直接返回
// 否则:
// 1. 先初始化父类(递归 EnsureInitialized on superclass)
// 2. 执行 <clinit> 方法(类的静态初始化块和静态字段初始化)
// 3. 设置初始化状态为 kInitialized
}

2.3 Class.forName vs ClassLoader.loadClass

特性 Class.forName ClassLoader.loadClass
是否执行 <clinit> 是(默认 initialize=true) 否(懒初始化,首次使用时执行)
使用场景 JDBC 驱动、需要静态初始化的类 通用的类加载模型、Spring 容器
底层实现 调用 forName0 Native 方法 委派给父 ClassLoader
加载锁 class_load_lock_ 与 forName 共用同一个锁

为什么 JDBC 使用 Class.forName? JDBC 驱动在静态初始化块中将自己注册到 DriverManager:

public class com.mysql.jdbc.Driver {
static {
java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());
}
}

Class.forName("com.mysql.jdbc.Driver") 触发 <clinit>,从而完成注册。如果使用 ClassLoader.loadClass<clinit> 不会执行,驱动不会被注册。

2.4 DexFile.FindClassDef:DEX 文件中的类查找

在 ART 的低层,DexFile::FindClassDef 是通过在 DEX 文件的 type_ids 数组中进行二分搜索实现的。因为 DEX 文件的 type_ids 按字符串排序(基于 string_ids 的排序),所以可以通过二分搜索实现 O(log n) 的类查找。

查找流程:

  1. 将 descriptor(如 Lcom/example/Foo;)在 string_ids 中二分搜索,获取 string_id 索引。
  2. type_ids 中查找 descriptor_idx 等于该 string_id 索引的条目,获取 type_id 索引。
  3. class_defs 中查找 class_idx 等于该 type_id 索引的条目,即为目标类的 class_def。

这个三段式查找是 DEX 格式”统一索引表”设计的直接应用。

三、Method.invoke 的完整调用流程

Method.invoke 是反射调用的核心。其流程非常复杂,涉及权限检查、参数适配、调用分派和可能的 JIT 优化。

3.1 Java 层入口

libcore/ojluni/src/main/java/java/lang/reflect/Method.java 中:

public Object invoke(Object obj, Object... args) throws ... {
// 1. 访问权限检查
if (!override) { // override 由 setAccessible(true) 设置
// 检查调用者是否有权访问此方法
// 涉及类层级关系和模块访问检查
Reflection.verifyMemberAccess(caller, declaringClass, ...);
}

// 2. 参数数量检查
if (args.length != parameterTypes.length) {
throw new IllegalArgumentException("wrong number of arguments");
}

// 3. 委托给 Native 实现
return invokeNative(obj, args, declaringClass, parameterTypes,
returnType, slot, ...);
}

3.2 Native 层:InvokeMethod

art/runtime/reflection.cc 中的 InvokeMethod 函数(简化逻辑):

JValue InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa,
jobject javaMethod, jobject javaReceiver,
jobject javaArgs, size_t num_frames) {
// 1. 从 java.lang.reflect.Method 对象中提取 ArtMethod* 指针
ArtMethod* method = ArtMethod::FromReflectedMethod(soa, javaMethod);

// 2. 检查方法是否可访问(access_flags 验证)
if (!VerifyObjectInClass(receiver, declaring_class)) {
ThrowIllegalAccessException(...);
}

// 3. 将 javaArgs(Object[])按方法签名解包
// - 基本类型参数需要拆箱:Integer → int, Long → long 等
// - 检查 null 参数是否可赋值给参数类型
// - 构造参数列表(在栈上分配或使用 Thread 的临时缓冲区)
uint32_t args_size = NumArgArrayValues(method->GetShorty());
char args_array[args_size]; // 或堆分配

// 4. 通过 ArtMethod::Invoke 执行目标方法
method->Invoke(soa.Self(), args_array, args_size, &result, method->GetShorty());

// 5. 包装返回值(如果是基本类型,需要装箱:int → Integer 等)
return BoxPrimitiveResult(result, return_type);
}

步骤 3(拆装箱)是反射调用最大的性能瓶颈之一。对于方法 void foo(int a, long b, String c)javaArgsObject[]{Integer(1), Long(2L), "hello"},需要将其转换为原始值 int 1, long 2L, String ref 并放入参数缓冲区。同样的,返回值如果是基本类型也需要装回包装类。

3.3 ArtMethod::Invoke 的执行

ArtMethod::Invokeart/runtime/art_method.cc)是执行方法的通用入口:

void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size,
JValue* result, const char* shorty) {
// 1. 检查是否有编译代码(AOT 或 JIT)
if (HasCompiledCode()) {
// 2a. 直接跳转到 entry_point_from_quick_compiled_code_
// 这是最快速的路径——直接执行本机代码
entry_point_from_quick_compiled_code_(self, args, result, shorty);
} else {
// 2b. 走解释器路径
// 在 ART 的 interpreter 中逐条执行字节码
interpreter::EnterInterpreterFromInvoke(self, this, args, result, ...);
}
}

entry_point_from_quick_compiled_code_ 是一个函数指针,指向 AOT(dex2oat 编译)或 JIT 编译生成的本机代码。在 ART 中,方法的执行可以在编译代码和解释器之间无缝切换——这也适用于反射调用。当反射调用目标是一个热点方法时,ART 的 JIT 会编译该方法的普通入口,而反射路径也会自动使用这个编译版本。

3.4 FromReflectedMethod 的实现

ArtMethod::FromReflectedMethod 是从 java.lang.reflect.Method 对象中提取 ArtMethod* 指针的关键方法。它的工作方式利用了 Java 对象中的隐藏字段:

在 ART 中,java.lang.reflect.Method 对象实际上包含一个 artMethod 字段(或通过 declaring class 和方法索引间接查找)。最直接的实现是将 ArtMethod* 存储在 Method 对象的某个隐藏字段中(通常是一个 long 类型的字段,在 64 位系统中存储完整的指针,在 32 位系统中有特殊的压缩编码)。

这种设计意味着反射调用只需要从 Method 对象中读取这个指针字段(一次内存访问),然后就可以调用 ArtMethod::Invoke——而不需要每次都重新从 DEX 文件中查找方法。

四、反射性能开销的根源

4.1 开销分解

反射调用比直接调用慢的原因是多维度的。以下按开销从大到小排序:

1. 参数拆装箱(Boxing/Unboxing)

反射调用使用 Object[] 作为参数载体。对于基本类型参数:

应用代码:    int result = method.invoke(42, "hello");
字节码等价: Integer boxed = Integer.valueOf(42);
Object[] args = {boxed, "hello"};
Object result = method.invoke(obj, args);
int unboxed = ((Integer) result).intValue();

每一次基本类型参数的装箱/拆箱都涉及对象分配(Integer.valueOfDouble.valueOf 等)和内存间接访问。对于热路径方法,这种开销可能占到反射调用总开销的 40-60%。

2. 权限检查(Access Check)

每次 Method.invoke 都会调用 Reflection.verifyMemberAccess,检查调用者是否有权限访问目标方法。这涉及:

  • 遍历方法的 access_flags(public/protected/private/package)
  • 遍历调用者的类层级关系
  • 检查 Java 9+ 的模块访问规则(--add-opens

即使调用了 setAccessible(true),后续的 invoke 仍然需要执行一些简化后的检查(检查 override 标志、检查 receiver 类型匹配等)。

3. 额外间接层(Indirection)

Method.invoke → JNI 桥接 → InvokeMethodArtMethod::Invoke → 目标方法。每个环节都有栈帧创建/销毁和参数复制的开销。而直接调用通常是:callercallee,仅一层。

4. JIT 内联受阻(Inlining Barrier)

JIT 编译器难以对反射调用进行内联,因为它看不到调用的实际目标——ArtMethod* 在编译期未知。即使 JIT 在运行时观测到该反射调用点总是调用同一个方法,内联反射调用比内联普通虚方法调用复杂得多(需要在编译代码中处理反射的参数打包协议)。

4.2 各场景的性能数据(近似)

调用方式 相对耗时 说明
直接调用 1x (~1-2 ns) 基线
反射(cached Method, 无 inflation) ~50-100x 每次需拆装箱、访问检查、JNI 桥接
反射(cached Method, 已 inflation) ~2-5x inflation 消除了大部分开销
反射(未 cached Method) ~200-500x 每次 invoke 前需要 getDeclaredMethod
MethodHandle.invoke ~2-10x 无参数打包,但仍需 MethodHandle 间接调用
VarHandle ~1.1-1.5x 近乎原生性能,但仅限字段访问

五、反射膨胀(Reflection Inflation)优化

5.1 工作原理

反射膨胀(Reflection Inflation)是 JVM 对高频反射调用的关键优化,原理类似于 JIT 的热点编译:

阶段 1: 初次调用(慢速路径)

最初的 15 次(或由 JVM 参数控制的阈值)反射调用走完整的慢速路径:

Method.invoke → JNI → InvokeMethod → ArtMethod::Invoke → 目标

ART 的 JIT profiling 在此期间记录该调用点的调用次数。

阶段 2: 达到阈值

当调用次数超过阈值(ART 中约 15 次,HotSpot 中由 sun.reflect.inflationThreshold 控制),JIT 为这个调用点生成一个专用的”访问器”(accessor)或”膨胀桩”(inflation stub)。

阶段 3: 膨胀后调用(快速路径)

访问器是一小段本机代码,它执行以下操作:

  1. 从 Method 对象中提取 ArtMethod* 指针。
  2. 加载 receiver(this 或 null)。
  3. 按照目标方法的签名直接加载参数(不需要拆装箱)。
  4. 直接调用 entry_point_from_quick_compiled_code_
  5. 如果需要,箱装返回值。

关键优势:访问器静态地知道目标方法的签名(参数类型和返回类型),因此可以直接从 Object[] 中按索引读取参数并做类型检查,避免了运行时遍历参数数组和反射类型匹配的开销。

5.2 HotSpot 中的实现对比

HotSpot 的 inflation 策略与 ART 有所不同:

  • HotSpot:使用 sun.reflect.MethodAccessorGenerator(一个字节码生成器),生成 Java 字节码来实现访问器,然后交给 JIT 编译。这意味着 HotSpot 的 inflation 实际上是生成一段等价于 “receiver.method(arg0, arg1, …)” 的字节码——但接收者和参数来自反射框架的包装对象。
  • ART:由于 Android 的 class 文件在安装时已转换为 DEX,不支持运行时字节码生成(这是安全限制的一部分,防止运行时生成可执行代码的恶意行为)。因此 ART 的 JIT 直接生成本机代码作为访问器,跳过了字节码中间表示。

5.3 Inflation 的限制

Inflation 不是万能的。以下情况 inflation 可能不生效或效果有限:

  • 每次都调用了不同的 Method 对象(如 clazz.getDeclaredMethod(name, types) 在循环中)。Inflation 是绑定在(调用点, Method对象)上的——如果 Method 对象变了,inflation 需要重新触发。
  • 参数和返回值全是对象类型(无基本类型),inflated 路径和普通路径的差距不大(因为没有拆装箱开销可以消除)。
  • 目标方法执行时间很长(如 I/O 操作),反射框架的开销相对于方法体执行时间可以忽略不计,inflation 收益不明显。

六、AccessibleObject.setAccessible 的机制

6.1 实现原理

AccessibleObject 是所有可反射访问对象(FieldMethodConstructor)的基类。setAccessible(true) 设置的是其内部的 override 布尔标志。

在 ART 中,reflection.ccMethod_setAccessible JNI 方法:

static void Method_setAccessible(JNIEnv* env, jobject javaMethod, jboolean flag) {
// 设置 Method 对象的 override 字段为 true/false
SetFieldBoolean(javaMethod, WellKnownClasses::java_lang_reflect_AccessibleObject_override, flag);
}

设置 override = true 后,后续的 Method.invoke 调用会跳过 Reflection.verifyMemberAccess 的类层级检查:

boolean checkAccess = !override;
if (checkAccess) {
Reflection.verifyMemberAccess(caller, declaringClass, ...);
}

6.2 安全限制

即使 setAccessible(true) 也无法完全绕过所有安全检查:

  • 模块系统(Java 9+):即使 override=true,如果目标类所在的模块没有 opens 当前调用者的包,则访问仍被拒绝。模块访问检查在 override 检查之前进行。
  • 安全管理器(SecurityManager,Java 17 废弃)setAccessible 本身会触发 checkPermission(ACCESS_PERMISSION),受 SecurityManager 管控。
  • 在 Android 中:Android 没有 SecurityManager,模块系统在 Android 中也较为有限(主要应用在系统内部框架层面),因此 setAccessible(true) 在 Android 应用开发中通常能生效。

七、MethodHandle 和 VarHandle:反射的演进

7.1 MethodHandle(Java 7+)

java.lang.invoke.MethodHandle 是 JSR 292 引入的”类型化的方法引用”,它在创建时确定了类型签名,调用时不需要参数拆装箱:

// 反射:每次 invoke 都有拆装箱开销
Method m = String.class.getDeclaredMethod("length");
int len = (Integer) m.invoke("hello"); // 返回 Object → casting → unboxing → int

// MethodHandle:类型安全,无拆装箱
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(String.class, "length",
MethodType.methodType(int.class));
int len = (int) mh.invokeExact("hello"); // 精确类型匹配,直接返回 int
// 或者
int len = (int) mh.invoke("hello"); // invoke 可以处理类型转换

MethodHandle 的核心优势:

  1. 签名精确:MethodType 在 Lookup 时确定,invokeExact 要求调用端类型完全匹配,编译期即可进行类型安全校验。
  2. 可组合MethodHandles.filterArgumentsMethodHandles.collectArgumentsMethodHandles.guardWithTest 等操作可以将 MethodHandle 组合成更复杂的调用逻辑,而不需要编写额外的胶水代码。
  3. JIT 友好:MethodHandle 的调用链(lambda form)可以被 JIT 编译器递归内联,最终优化为直接调用。

在 ART 中,MethodHandle 的支持通过 art/runtime/native/java_lang_invoke_MethodHandleImpl.cc 实现。ART 的 MethodHandle 实现最终也是委托给 ArtMethod::Invoke,但跳过了反射框架的参数拆装箱阶段——因为 MethodHandle 在创建时已经记录了 MethodType,调用时不再需要运行时类型推导。

7.2 VarHandle(Java 9+)

java.lang.invoke.VarHandle 是 Java 9 引入的用于细粒度内存访问和 CAS 操作的句柄,可替代 sun.misc.Unsafe 的字段操作:

class Counter {
volatile int value;
}

// 传统 Unsafe 方式(危险,无类型安全)
long offset = unsafe.objectFieldOffset(Counter.class.getDeclaredField("value"));
unsafe.compareAndSwapInt(counter, offset, 0, 1);

// VarHandle 方式(类型安全)
VarHandle VH = MethodHandles.lookup()
.findVarHandle(Counter.class, "value", int.class);
VH.compareAndSet(counter, 0, 1); // CAS 操作
VH.getVolatile(counter); // volatile 读
VH.setRelease(counter, 2); // release 写

VarHandle 的访问模式(Memory Ordering Modes):

访问模式 内存排序保证 等效 Java 关键词
plain 无保证 普通变量访问
opaque 操作本身的原子性
acquire 后续操作不会重排序到此操作之前
release 之前操作不会重排序到此操作之后
volatile acquire + release volatile 变量
compareAndSet 完全内存屏障 AtomicInteger CAS

VarHandle 的性能接近直接字段访问(1.1-1.5x),因为它直接映射到 CPU 的原子指令(如 x86 的 CMPXCHG、ARM64 的 LDXR/STXR),不经过任何 JNI 包装或反射框架。

在 ART 中,VarHandle 通过 sun.misc.Unsafe 的内部实现(art/runtime/native/sun_misc_Unsafe.cc)提供原子操作支持。与 HotSpot 的 JIT 直接编译不同,ART 的 VarHandle 在某些路径上通过解释器或 JNI 边界,性能比 HotSpot 略低,但依然是可用的高性能方案。

7.3 三者的性能对比

特性 反射 (Method.invoke) MethodHandle VarHandle
引入版本 Java 1.1 Java 7 (JSR 292) Java 9 (JEP 193)
参数包装 Object[](有拆装箱) 精确 MethodType(无拆装箱) N/A(直接字段访问)
性能(vs 直接调用) ~2-5x(inflated) ~2-10x ~1.1-1.5x
类型安全 弱(运行时检查) 强(MethodType 在创建时确定)
可内联性 低(JIT 受限于 call site) 高(LambdaForm 可递归内联) 极高(直接映射为原子指令)
功能范围 全面(方法、字段、构造、数组、注解) 方法调用 + 字段访问 + 组合 字段的原子/volatile 访问模式

八、动态代理(Dynamic Proxy)

8.1 代理类生成的字节码

java.lang.reflect.Proxy 可以在运行时动态生成一个实现了指定接口的代理类:

InvocationHandler handler = (proxy, method, args) -> {
System.out.println("before " + method.getName());
return method.invoke(target, args);
};

MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
MyInterface.class.getClassLoader(),
new Class<?>[]{MyInterface.class},
handler
);

Proxy.newProxyInstance 的工作流程:

  1. 调用 Proxy.getProxyClass(loader, interfaces) 为给定的接口集合生成或查找代理类。
  2. 通过反射获取代理类的 Constructor(InvocationHandler)
  3. 使用该构造方法创建代理实例(传入 handler)。

8.2 代理类的内部结构

代理类的名字遵循 $ProxyN 模式(N 为递增序号)。在 ART 中,代理类的生成在 libcore/ojluni/src/main/java/java/lang/reflect/Proxy.java 中实现,它通过内部 API 调用 ClassLoader.defineClass 来生成代理类的 DEX 字节码。

代理类的关键特征:

// 简化的等效代码(实际是 DEX 字节码生成,而非 Java 源码)
public final class $Proxy0 extends Proxy implements MyInterface {
private static Method m1; // MyInterface 的方法 1
private static Method m2; // MyInterface 的方法 2
// ... equals, hashCode, toString

static {
// 静态初始化:缓存 Method 对象
m1 = Class.forName("MyInterface").getMethod("doSomething", ...);
m2 = Class.forName("MyInterface").getMethod("getValue", ...);
}

public void doSomething(String arg) {
super.h.invoke(this, m1, new Object[]{arg});
}

public int getValue() {
return (Integer) super.h.invoke(this, m2, null);
}
}

注意:代理类的每个接口方法都通过 InvocationHandler.invoke 转发,而 InvocationHandler.invoke 的第三个参数仍然是 Object[]——因此动态代理内部的调用路径仍然是反射风格的(有拆装箱和数组打包开销)。

九、Field 反射读写与 Constructor.newInstance

反射不仅用于方法调用,字段(Field)和构造方法(Constructor)的反射操作同样重要。

9.1 Field.get/set 的实现

Field.get(Object obj)Field.set(Object obj, Object value) 的实现与 Method.invoke 类似:

// art/runtime/reflection.cc — Field_get 简化
JValue Field_get(JNIEnv* env, jobject javaField, jobject javaObj) {
// 1. 从 Field 对象中提取 ArtField* 指针
ArtField* field = ArtField::FromReflectedField(javaField);

// 2. 访问权限检查(如未 setAccessible)
VerifyAccess(self, obj, field, ...);

// 3. 读取字段值
// 对于实例字段:obj + field_offset(直接内存偏移读取)
// 对于静态字段:class_static_storage + field_offset
JValue result;
field->Get(obj, &result);

// 4. 如果是基本类型,装箱返回
return BoxPrimitive(result, field->GetType());
}

字段访问比方法调用快得多——因为不需要解析参数数组,不需要构造调用栈。基本类型字段的反射读/写主要瓶颈在拆装盒操作(int ↔ Integer),而引用类型字段的反射读/写几乎没有额外开销(除了权限检查和 JNI 边界)。

性能对比:

操作 相对耗时 (vs 直接)
直接字段读 1x (~1ns)
Field.get(int 字段) ~10-20x(装箱开销)
Field.get(Object 字段) ~3-5x(权限检查 + JNI)
Field.get(已 setAccessible) ~2-3x

9.2 Constructor.newInstance

Constructor.newInstance(Object... args) 的执行路径与 Method.invoke 几乎相同,差异仅在于:

  • 调用前需要先给 ArtMethod 分配对象(通过堆分配器 TLAB)。
  • 调用的是 <init> 方法而非普通方法。
  • 返回的是新创建的对象(而非 <init> 的 void 返回值)。

反射创建对象的开销 = 对象分配(TLAB fast path: ~10-20ns)+ Method.invoke 开销。

十、JIT Profiling 与 Inflation 触发机制

10.1 计数器的维护

ART 的 JIT 系统(art/runtime/jit/jit.cc)通过方法入口计数器来跟踪调用频率。对于反射调用点,计数器按 (CallSite, ArtMethod*) 键追踪:

每个反射调用点维护:
┌────────────────────────────────┐
│ 调用点 PC (program counter) │
│ 目标 ArtMethod* 指针 │
│ counter: uint16_t │ ← 当前调用计数
│ threshold: uint16_t │ ← inflation 阈值(默认 15)
│ flags: uint8_t │ ← 是否已 inflation / 是否热门
└────────────────────────────────┘

每次 Method.invoke 执行时,JIT profiling 代码递增对应条目的计数器。当 counter >= threshold 时,JIT 触发编译请求。

10.2 dex2oat 的 AOT 预编译

ART 的 dex2oat(AOT 编译器)在安装时可以对已知的反射目标进行预编译。如果 art/compiler/optimizing/ 的分析阶段检测到类中存在高频反射调用模式(比如通过 Class.forName 加载某个特定类,然后调用 getDeclaredMethod("特定方法名")),dex2oat 可以提前准备相应的快速路径,免除前 15 次 warm-up 的开销。

这种优化受限于 dex2oat 无法进行运行时 profiling(只能做静态分析和启发式预测),因此实际效果有限。主要的 inflation 收益仍然来自运行时 JIT。

十一、反射对 GC 和内存的影响

反射调用产生的临时对象对 GC 有不容忽视的压力:

  • 每次 invoke:至少分配 Object[](方法参数)和可能的装箱对象(每个基本类型参数 1 个包装对象)。
  • 每次 getDeclaredMethod/getDeclaredField:分配一个新的 Method/Field 对象。
  • 动态代理:每次代理方法调用分配 Object[args]

在高频反射调用场景中(如 JSON 反序列化库的早期实现),这些临时对象的分配速率可达 每秒数百万次,对 GC 造成显著压力。现代 JSON 库(如 Moshi、Kotlin Serialization)使用编译期代码生成来替代反射,从根本上消除了这一开销。

ART 的 TLAB(Thread-Local Allocation Buffer)虽然能加速小对象分配,但无法消除 GC 回收这些临时对象的成本。这是”反射性能差”的隐性维度——不仅仅是 CPU 开销,还有 GC 暂停对帧率的影响。

十二、Array 反射操作

java.lang.reflect.Array 类提供了数组的动态创建和访问能力:

// 动态创建数组
int[] arr = (int[]) Array.newInstance(int.class, 10);

// 动态读取
int value = Array.getInt(arr, 3);

// 动态设置
Array.setInt(arr, 3, 42);

这些操作在 ART 中直接映射到 DEX 的数组指令(newarrayialoadiastore),只是需要通过反射的权限检查和类型验证。与 Method.invoke 不同,数组反射不需要参数拆装箱(因为 Array.getInt/setInt 等方法已经按基本类型重载),因此数组反射的性能比方法反射好得多——仅比直接数组访问慢 2-3 倍。在 art/runtime/native/java_lang_reflect_Array.cc 中可以找到对应的 JNI 实现。

反射数组与泛型的一个重要交互:Array.newInstance(Class<?> componentType, int length) 是解决泛型数组创建问题的标准方法。当泛型类型参数 T 需要创建数组时,由于 new T[n] 被编译器禁止,代码使用 (T[]) Array.newInstance(componentTypeClass, n) 作为替代方案,这是 Java 类型令牌模式的一种典型应用。

十三、Constructor 的反射与对象创建

Constructor.newInstance() 在 Android 开发中的一个重要使用场景是 Fragment 的无参构造方法实例化。Android Framework 通过反射调用 Fragment.instantiate(Context, String) 来恢复被销毁重建的 Fragment 实例:

// FragmentFactory / Fragment 内部
public static Fragment instantiate(Context context, String fname) {
Class<?> clazz = Class.forName(fname);
Constructor<?> constructor = clazz.getConstructor();
return (Fragment) constructor.newInstance();
}

这就是为什么 Android Framework 要求 Fragment 必须有一个公开的无参构造方法——Framework 通过反射来创建 Fragment 实例,而无参构造方法确保 newInstance() 的成功调用。从 AndroidX Fragment 1.3.0 开始,FragmentFactory 允许开发者自定义 Fragment 的构造逻辑,但底层仍是基于反射机制。

十四、实战建议与常见陷阱

9.1 正确的反射使用模式

缓存 Method 对象

// 错:每次调用都查找 Method
for (int i = 0; i < 10000; i++) {
Method m = MyClass.class.getDeclaredMethod("compute", int.class);
m.invoke(obj, i);
}

// 对:预先缓存 Method
static final Method COMPUTE = MyClass.class.getDeclaredMethod("compute", int.class);
for (int i = 0; i < 10000; i++) {
COMPUTE.invoke(obj, i); // 前 15 次慢速,之后 inflation 生效
}

getDeclaredMethod 的开销包括:遍历方法表(线性搜索)、创建 Method 对象(堆分配)、安全权限检查。缓存 Method 对象可消除这些开销。

尽早 setAccessible

// 仅设置一次
static final Method PRIVATE_METHOD = ...;
static {
PRIVATE_METHOD.setAccessible(true);
}

使用 MethodHandle 替代高频反射

// 替代方案(性能更好,但需要提前知道签名)
MethodHandle mh = MethodHandles.lookup()
.findVirtual(MyClass.class, "compute", MethodType.methodType(int.class, int.class));
for (int i = 0; i < 10000; i++) {
int result = (int) mh.invokeExact(obj, i); // 类型安全,无拆装箱
}

9.2 常见陷阱

陷阱 说明 解决
getMethods vs getDeclaredMethods getMethods 返回所有公共方法(含继承);getDeclaredMethods 仅返回当前类声明的方法 明确需求
可变参数方法的反射调用 method.invoke(obj, new Object[]{new Object[]{1, 2}}) vs method.invoke(obj, 1, 2) 了解 Java 的自动变参展开规则
基本类型返回值空指针 反射返回的 Object 为 null 时,拆箱为基本类型会抛出 NPE 先检查 null,或使用包装类接收
混淆导致反射失败 ProGuard/R8 可能重命名或移除反射目标 使用 -keep 规则保留反射调用的类/方法/字段
getGenericReturnType 被剥离 ProGuard 去除 Signature 属性 -keepattributes Signature
桥方法的干扰 getDeclaredMethod 可能返回桥方法(ACC_BRIDGE)而非实际方法 检查 method.isBridge(),必要时使用 getMethod

十、AOSP 关键源码路径总结

文件 功能
art/runtime/reflection.cc 反射核心:InvokeMethod、InvokeConstructor、VerifyObjectInClass、所有访问检查逻辑
art/runtime/art_method.h ArtMethod 结构定义:entry_point_from_quick_compiled_code_、access_flags
art/runtime/art_method.cc ArtMethod::Invoke、ArtMethod::FromReflectedMethod
art/runtime/class_linker.cc FindClass、DefineClass、LinkClass、EnsureInitialized、vtable/iftable 构建
art/runtime/native/java_lang_reflect_Method.cc Method.invoke 的 JNI 原生实现
art/runtime/native/java_lang_reflect_Field.cc Field.get/set 的 JNI 实现
art/runtime/native/java_lang_Class.cc Class.forName、Class.newInstance 的 JNI 实现
art/runtime/native/sun_misc_Unsafe.cc Unsafe 操作(VarHandle 底层依赖)
art/runtime/native/java_lang_invoke_MethodHandleImpl.cc MethodHandle 的 ART 实现
art/runtime/jit/jit.cc JIT profiling:跟踪反射调用频次,触发 inflation
art/runtime/interpreter/ 解释器:未编译的 ArtMethod::Invoke 走此路径
libcore/ojluni/src/main/java/java/lang/reflect/Method.java Java 层反射 Method 类
libcore/ojluni/src/main/java/java/lang/reflect/Field.java Java 层反射 Field 类
libcore/ojluni/src/main/java/java/lang/reflect/Proxy.java 动态代理类生成
libcore/ojluni/src/main/java/java/lang/invoke/MethodHandle.java MethodHandle Java API
libcore/ojluni/src/main/java/java/lang/invoke/VarHandle.java VarHandle Java API
art/runtime/native/java_lang_reflect_Array.cc Array 反射操作(newInstance、get/set)

调试反射问题的关键日志和工具:

  • adb logcat | grep -E "reflect|Reflection":查看 ART 反射路径的运行时日志。
  • art/tools/dexfuzz/:DEX 文件模糊测试工具,可用于测试反射边界的健壮性。
  • 使用 StrictMode 检测主线程上的反射调用:StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectReflection().penaltyLog().build())

面试问答

Q1:Class.forName 在 ART 中的完整加载链路是什么?与 ClassLoader.loadClass 有什么区别?

A:完整链路为:Java 层 Class.forNameClassLoader.loadClassBaseDexClassLoader.findClassDexPathList.findClass 遍历 dex → JNI 层 DefineClassart/runtime/native/dalvik_system_DexFile.cc)→ ClassLinker::DefineClass 解析 DEX 文件、分配 Class 对象、插入 ClassTable → ClassLinker::LinkClass 构建 vtable/iftable → ClassLinker::EnsureInitialized 执行 <clinit>。区别在于 Class.forName 默认执行类的初始化(执行 <clinit>),而 ClassLoader.loadClass 默认不执行初始化,仅在首次实际使用时才初始化(懒加载)。Class.forName 常用于 JDBC 驱动加载等需要触发静态初始化块的场景。两个方法在底层共用同一个 class_load_lock_ 防止并发重复加载。

Q2:Method.invoke 的性能瓶颈在哪里?ART 如何优化?

A:瓶颈有四个方面:(1) 每次调用的参数类型检查和拆装箱(Object[] 到基本类型的拆箱、返回值的装箱),这占总开销的 40-60%;(2) 每次调用的访问权限检查(Reflection.verifyMemberAccess);(3) 多层级间接调用(Java→JNI→reflection.cc→ArtMethod::Invoke);(4) JIT 无法内联未知调用目标。ART 通过”反射膨胀”(reflection inflation)优化——当一个反射方法被频繁调用(约 15 次阈值),JIT 为该调用点生成专用的本机代码访问器,直接跳转到目标 ArtMethod 的编译入口,绕过大部分检查逻辑和拆装箱开销,性能可接近直接调用的 2-5x。关键优化是访问器静态地知道目标方法的签名(参数类型在 inflation 时已确定),因此不需要运行时类型推导。

Q3:ArtMethod 结构在反射中扮演什么角色?

A:ArtMethodart/runtime/art_method.h)是 ART 中表示一个方法的运行时核心数据结构。每个 java.lang.reflect.Method 对象内部持有一个指向对应 ArtMethod 的指针(通过隐藏字段或索引间接存储)。ArtMethod 包含了方法的所有运行时信息:入口点指针 entry_point_from_quick_compiled_code_(指向 AOT 或 JIT 编译的代码)、DEX 方法索引、access flags、方法在 vtable 中的偏移、类引用等。当 Method.invoke 执行时,首先通过 ArtMethod::FromReflectedMethod 提取这个指针,然后调用 ArtMethod::Invoke 执行方法。ArtMethod 的设计使得 ART 可以在 AOT/JIT 编译代码和解释器执行之间无缝切换——这种灵活性同样适用于反射调用。

Q4:如何通过反射获取泛型返回值类型?

A:使用 Method.getGenericReturnType()(而非 getReturnType())。后者返回的是擦除后的类型(如 List),前者读取 class 文件中方法的 Signature 属性,返回完整的泛型类型(如 List<String>)。对于字段同理使用 Field.getGenericType()。这些方法的底层实现引用了 java.lang.reflect 包中的 GenericSignatureFormatError 处理逻辑,读取 class 文件的 Signature 属性字符串并解析为 TypeVariableParameterizedType 等接口实现。如果 class 文件的 Signature 属性被 ProGuard 剥离(未配置 -keepattributes Signature),则 getGenericReturnType() 会回退到擦除类型。

Q5:MethodHandle 和反射 Method.invoke 的核心区别是什么?什么时候用哪个?

A:核心区别有四点。(1) 签名:MethodHandle 在创建时确定 MethodType(参数类型和返回类型),调用时不需要拆装箱;反射的 invoke 总是接收 Object[] 参数,需要运行时类型检查。(2) 性能:MethodHandle 比未 inflation 的反射快得多(2-10x vs 50-100x),但 inflated 后的反射(2-5x)与 MethodHandle 相当。(3) 可组合性:MethodHandle 有丰富的组合器(filterArguments、insertArguments、guardWithTest),可以在调用点进行函数式组合,反射不具备此能力。(4) 使用场景:反射适合需要动态发现方法(方法名在编译期未知、或需要遍历所有方法)的场景;MethodHandle 适合方法名已知、仅在调用方式上需要动态性的场景(如实现一个高性能的调用分发器)。

Q6:反射的 Inflation 优化在什么情况下不生效?

A:Inflation 不生效的主要原因:(1) 调用点频繁更换不同的 Method 对象(如在循环中调用 getDeclaredMethod),因为 inflation 绑定在特定的调用点和 Method 对象组合上;(2) 反射调用的目标方法执行时间很长(如磁盘 I/O),反射框架的开销占总耗时比例很小,inflation 无法带来显著提升;(3) 方法的参数和返回值全是引用类型(无基本类型),inflation 消除的拆装箱收益有限;(4) 调用次数未能达到阈值(< 15 次),inflation 根本不会被触发;(5) 系统的 JIT 被禁用或处于低配状态(如在模拟器或调试模式下)。

Q7:动态代理在 Android 中的实现与 HotSpot 有何不同?

A:在 HotSpot 中,动态代理通过 sun.misc.ProxyGenerator 生成 Java 字节码(.class 文件格式),然后调用 Unsafe.defineClass 将其定义为一个新的类。在 ART 中,由于 Android 不支持运行时字节码生成(.class 格式),动态代理通过 libcore 中的 Proxy.java 直接生成 DEX 字节码,然后通过 ClassLoader.defineClass 注册。两者的接口方法转发逻辑相似——都通过 InvocationHandler.invoke(proxy, method, args) 调用,因此性能特征也相似:每次代理方法调用都涉及反射风格的 Object[] 参数打包。对于性能敏感的代理场景,考虑使用编译期代码生成(如 Dagger、代码生成注解处理器)来替代运行时动态代理。

Q8:为什么反射调用中缓存 Method 对象如此重要?

A:getDeclaredMethod 的执行成本远高于一次 invoke。它需要:(1) 遍历方法表进行线性搜索(对比名称和参数类型);(2) 在找到方法后创建一个新的 Method 对象(堆分配);(3) 执行安全权限检查。对于一个类有 20 个方法,每次 getDeclaredMethod 都需遍历和比较。而缓存的 Method 对象可以直接调用 invoke——尤其是经过 inflation 后,invoke 的开销降低到直接调用的 2-5x。在实测中,不使用缓存的反射调用(每次 getDeclaredMethod + invoke)可以比直接调用慢 200-500 倍,而缓存后的 inflated invoke 仅慢 2-5 倍。这个 100 倍的差距完全是方法查找和对象创建的开销。

打赏
  • 微信
  • 支付宝

评论