目录
  1. 1. 一、Class.forName 的加载链路
  2. 2. 二、Method.invoke 的调用流程
    1. 2.1. 2.1 入口到 Native 层
    2. 2.2. 2.2 ART 中的 Native 实现
    3. 2.3. 2.3 ArtMethod::Invoke 的执行
  3. 3. 三、反射的性能开销与优化
    1. 3.1. 3.1 反射为何慢
    2. 3.2. 3.2 反射膨胀(Reflection Inflation)
    3. 3.3. 3.3 setAccessible 的性能代价
  4. 4. 四、与字节码的正常调用对比
  5. 5. 面试问答
【深入理解JVM字节码】第六篇、反射实现原理

一、Class.forName 的加载链路

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

在 ART 中,完整链路如下:

  1. Java 层java.lang.Class.forName(className)libcore/ojluni/src/main/java/java/lang/Class.java)→ Class.forName(className, true, classLoader),内部调用 VMClassLoader.loadClass 或通过 ClassLoader.loadClass 委派。

  2. JNI 层ClassLoader.loadClassBaseDexClassLoader.findClassDexPathList.findClass → 遍历 dex elements,每项调用 DexFile.loadClassBinaryNamedefineClassNative。最终进入 Native 方法 DefineClass,实现在 art/runtime/native/dalvik_system_DexFile.cc

  3. ART Runtime 层dex_file.cc 中读取 DEX 文件 → ClassLinker::DefineClassart/runtime/class_linker.cc)负责实际工作:解析 class_def 项→加载父类→链接接口→分配 Class 对象→插入 ClassTable。整个过程涉及类加载锁(ClassLinker::class_load_lock_)避免并发加载同一个类。

  4. 链接与初始化ClassLinker::LinkClass 完成 vtable/iftable 构建、字段布局计算;ClassLinker::EnsureInitialized 执行 <clinit> 类初始化方法。

在字节码层面也有相应的调用方式。如果类名在编译期已知,ldc 指令可以加载一个 Class 常量(CONSTANT_Class_info)。但 Class.forName 路径接受运行时字符串参数,无法在编译期确定,因此必须走上述完整的类加载链路。

二、Method.invoke 的调用流程

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

2.1 入口到 Native 层

java.lang.reflect.Method.invoke(AOSP:libcore/ojluni/src/main/java/java/lang/reflect/Method.java)中,核心逻辑分三步:

  1. 访问权限检查:如果方法是私有或包级可见,检查 AccessibleObject.override 标志(即在代码中是否调用了 setAccessible(true))。如果未设置,调用 Reflection.verifyMemberAccess 进行调用者的成员访问检查。

  2. 参数类型转换:反射调用接收 Object[] 作为参数,此时需要检查参数数量是否正确、类型是否匹配。基本类型参数需要做装箱/拆箱(Integer.valueOf vs intValue())。

  3. 委托给 Native 实现:最终调用 Method.invoke(Object receiver, Object... args)nativeInvoke(Native 方法,在 ART 中实现)。

2.2 ART 中的 Native 实现

Native 实现在 art/runtime/reflection.ccInvokeMethod 函数中:

// art/runtime/reflection.cc(简化逻辑)
JValue InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa,
jobject javaMethod, jobject javaReceiver,
jobject javaArgs, size_t num_frames) {
// 1. 从 javaMethod 解码出 ArtMethod*
ArtMethod* method = ArtMethod::FromReflectedMethod(soa, javaMethod);

// 2. 检查方法是否可访问(access flags)
if (!VerifyAccess(...)) { ThrowIllegalAccessException(...); }

// 3. 将 javaArgs(Object[])转换为方法签名对应的实际参数
// 基本类型需要拆箱引用类型
// ...

// 4. 通过 ArtMethod::Invoke 执行方法
method->Invoke(soa.Self(), args, arg_size, &result, shorty);

return result;
}

其中 ArtMethod::FromReflectedMethod 是关键步骤——每个 java.lang.reflect.Method 对象内部持有一个 ArtMethod* 指针(通常存储在 shadow$_klass_declaringClassOf 等相关字段中)。在 ART 的 art/runtime/art_method.h 中,ArtMethod 是表示一个方法的运行时数据结构,包含入口点指针(entry_point_from_quick_compiled_code_)、DEX 中的方法索引、access flags 等。

2.3 ArtMethod::Invoke 的执行

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

  1. 检查方法是否已编译(AOT 或 JIT),如果有编译代码则直接跳转到 entry_point_from_quick_compiled_code_
  2. 否则走解释器(interpreter)路径。
  3. 处理同步方法(ACC_SYNCHRONIZED 标志)和 native 方法(ACC_NATIVE 标志)。

三、反射的性能开销与优化

3.1 反射为何慢

反射调用比直接调用慢的原因是多维度的:

  1. 类型检查与拆装箱:每次 invoke 都要检查参数个数和类型,基本类型需要拆箱(从 Integer 提取 int),调用完成后返回值需要装箱。

  2. 访问权限检查:每次调用都要执行 Reflection.verifyMemberAccess,涉及类层级关系遍历和访问标志判断。

  3. 额外间接层Method.invoke → JNI → reflection.cc::InvokeMethodArtMethod::Invoke,每个环节都有栈帧开销。

  4. JIT 内联受阻:JIT 编译器难以对反射调用进行内联优化,因为它看不到调用的实际目标——ArtMethod* 在编译期未知。

3.2 反射膨胀(Reflection Inflation)

从 Android 8.0 开始,ART 引入了反射膨胀机制来降低反射开销。当一个反射方法被频繁调用(超过一定阈值,默认为 15 次左右),ART 的 JIT 会为该 Method.invoke 调用点生成一段专用的字节码/汇编代码,直接跳转到目标 ArtMethod,绕过 InvokeMethod 的大部分检查和拆装箱逻辑。

这个机制在 HotSpot 中称为 “inflation”,在 ART 中的实现在 art/runtime/reflection.ccGetReflectionMethod 和 JIT 的相关路径中。膨胀后的反射调用近似于直接调用,性能可以达到直接调用的 70-90%。

3.3 setAccessible 的性能代价

AccessibleObject.setAccessible(true)art/runtime/reflection.cc 中通过 Field::SetAccessible / Method::SetAccessible 实现)会设置 override 标志为 true,跳过后续所有 invoke 过程中的访问检查。这能略微提升性能,但主要的性能瓶颈(拆装箱/间接层)仍存在。现代 JVM 中,setAccessible 已不会带来明显的性能差异,因为 JIT 的反射膨胀优化已经覆盖了权限检查的大部分开销。

四、与字节码的正常调用对比

将一个方法通过反射调用和直接调用的字节码对比:

直接调用

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

反射调用

ldc           #5  // class Test
ldc #6 // String "foo"
iconst_0
anewarray #7 // class Class
invokevirtual #8 // Method Class.getDeclaredMethod:(..)
// ... 然后是 Method.invoke 的调用生成字节码

可以看到,反射调用涉及常量池加载(类名、方法名字符串)、数组创建、多重方法调用,字节码的静态信息量远大于直接调用。直接调用中的 invokevirtual #5 能在编译期确定方法引用,运行时通过 vtable 查找后写入常量池缓存;而反射调用中方法信息仅为运行时字符串,无法借助编译期优化。


面试问答

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 驱动加载等需要触发静态初始化块的场景。

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

A:瓶颈有四个方面:每次调用的参数类型检查和拆装箱(Object[] 到基本类型的拆箱、返回值的装箱);每次调用的访问权限检查(Reflection.verifyMemberAccess);多层级间接调用(Java→JNI→reflection.cc→ArtMethod::Invoke);JIT 无法内联未知调用目标。ART 通过「反射膨胀」优化——当一个反射方法被频繁调用(约 15 次阈值),JIT 为该调用点生成专用代码直接跳转到目标 ArtMethod,绕过大部分检查逻辑,性能可接近直接调用的 70-90%。此外,开发者可以提前将 Method 对象缓存到 static final 字段,setAccessible(true) 一次以避免重复权限检查。

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() 会回退到擦除类型。

打赏
  • 微信
  • 支付宝

评论