# 反射机制

作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)


Java 反射(Reflection)机制允许在运行时动态获取类的信息或操作对象属性、方法、构造器等。
反射为框架设计、插件机制、IoC 容器和动态代理提供了基础能力,但也带来了性能和安全上的考量。本篇将系统梳理反射流程、性能损耗、优化策略及应用示例。

# 一、基本反射流程

核心流程示例

Class<?> clazz = Class.forName("com.example.User");

// 获取构造方法并创建对象
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object user = constructor.newInstance("张三", 18);

// 获取字段
Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
Object name = field.get(user);

// 获取方法并调用
Method method = clazz.getDeclaredMethod("getName");
method.setAccessible(true);
Object result = method.invoke(user);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

内部过程分析

  • Class.forName():通过类加载器加载 .class 字节码。

  • getDeclaredField/getDeclaredMethod:通过类的 内部反射缓存 查找字段/方法。

  • setAccessible(true):调用 ReflectionFactory 提升私有访问权限,绕过 Java 安全检查(在 JDK 9+ 会触发强警告)。

  • invoke():方法反射调用,会封装为 MethodAccessor 并通过 DelegatingMethodAccessorImpl 分发。

    • Method.invoke() ⬇
      |
      |--> DelegatingMethodAccessorImpl.invoke() ⬇
           |
           |--> NativeMethodAccessorImpl.invoke()(第一次调用)
                |
                |--> 调用本地代码执行实际的方法(通过 JNI 反射机制)
                |
                |--> 达到一定调用次数后(默认15次),切换为:
                       ⬇
                    MethodAccessorGenerator 生成字节码
                       ⬇
                    --> GeneratedMethodAccessorXXX.invoke()(JDK动态生成类)
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13

# 二、性能损耗分析

Java 反射提供了极大的灵活性,但其本质仍是“间接方法调用”,相比直接调用,其性能损耗主要来源于访问控制、方法查找、调用封装、JIT 逃逸等问题

# 为什么 Method.invoke() 会慢?

  1. 调用是通过泛型接口完成的,缺乏编译器内联优化

    Method.invoke() 的参数和返回值都是 Object 类型,JVM 无法进行静态分析和内联优化。

    public Object invoke(Object obj, Object... args)
    
    1
    • 基本类型要装箱(boxing)和拆箱(unboxing):

      int result = (Integer) method.invoke(obj, 1); // int ➜ Integer ➜ Object,再反向拆箱
      
      1
    • 泛型擦除导致类型不安全(编译器无法内联优化调用路径)。

  2. 反射调用流程层级多,涉及委托机制

    如前所述,Method.invoke() 实际上调用的是 MethodAccessor 接口的 invoke 方法,默认由 DelegatingMethodAccessorImpl 委托给 NativeMethodAccessorImpl 执行(前15次),再转为动态生成的 GeneratedMethodAccessorImpl

    虽然后续使用字节码生成器优化路径,但整个委托链和对象适配依然存在开销。

  3. 方法元信息查找耗时

    虽然 JVM 为 Method 对象缓存了元数据(通过 MethodAccessor),但初次反射依然需要查找方法、参数签名、访问控制等元信息,构建成本较高。

  4. 参数检查、类型转换和异常处理逻辑复杂

    • invoke() 方法内部会逐个参数进行校验、类型强制转换。
    • 反射调用的异常是被统一包裹成 InvocationTargetException 抛出,异常处理开销更高。

# JVM 的反射优化

为了提高反射性能,JVM 在反射调用中加入了缓存机制和动态字节码生成策略:

阶段 执行路径 特点
初始阶段(前15次) NativeMethodAccessorImpl 调用 native 方法,慢但安全
达到阈值后 GeneratedMethodAccessorImpl 动态生成类调用真实方法,绕过 native 和参数适配,提升性能
委托统一入口 DelegatingMethodAccessorImpl 委托至上面两个

☑️ 这些逻辑封装在 Method 类的 methodAccessor 字段中,一经初始化会缓存避免重复生成。

# setAccessible(true) 的影响

  1. 作用:跳过安全性检查

    默认情况下,Java 运行时会通过 checkAccess() 检查方法/字段的访问权限(public/protected/private),这会导致额外的权限校验和安全管理器检查:

    if (!override && !Reflection.quickCheckMemberAccess(clazz, modifiers)) {
        // 安全检查...
    }
    
    1
    2
    3

    调用 setAccessible(true) 后,override 标志被设置为 true,可直接访问字段/方法,省略这一块的消耗

  2. 是否能提高调用性能?

    仅影响访问权限检查阶段的性能,调用执行阶段依然使用反射机制,不会减少装箱拆箱、方法封装、异常捕获等成本。

    在低频反射场景下提升不明显,但在 高频调用反射私有字段或方法 时能降低开销。

    JDK 9 起,如果使用模块系统setAccessible(true) 会受到强限制,甚至抛出异常或警告。


# 三、MethodHandle 与反射对比

概述

MethodHandle 是 Java 7 引入的低层次方法引用机制,是 JDK 内部实现 Lambda、Stream 的基础。

  • 它是 JVM 级别的直接调用接口,性能比反射高得多。
  • 能被 JIT 编译优化为 几乎等同于直接调用的性能

示例:通过 MethodHandle 调用方法

import java.lang.invoke.*;

public class MethodHandleDemo {
    public static class User {
        public String sayHello(String name) {
            return "Hello, " + name;
        }
    }

    public static void main(String[] args) throws Throwable {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandle mh = lookup.findVirtual(User.class, "sayHello",
                MethodType.methodType(String.class, String.class));

        User user = new User();
        String result = (String) mh.invokeExact(user, "张三");
        System.out.println(result);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 四、典型应用:插件机制

反射是 面向接口+动态发现能力 的重要基础。在构建动态插件、框架或 IoC 容器时非常关键。

**示例:**插件注册与加载(简化版)

// 插件接口
public interface Plugin {
    void execute();
}

// SPI 加载机制
public class PluginLoader {
    public static void loadPlugins() throws Exception {
        File pluginDir = new File("plugins/");
        for (File file : pluginDir.listFiles()) {
            URLClassLoader loader = new URLClassLoader(new URL[]{file.toURI().toURL()});
            Class<?> clazz = loader.loadClass("com.plugin.MyPlugin");
            if (Plugin.class.isAssignableFrom(clazz)) {
                Plugin plugin = (Plugin) clazz.getDeclaredConstructor().newInstance();
                plugin.execute();
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

SPI 打破了类加载的双亲委派机制, 具体可参考JVM相关类容


# 五、性能优化策略

优化点 说明
缓存 Method/Field 对象 使用 ConcurrentHashMap 缓存反射信息,避免重复查找
避免频繁 setAccessible(true) 设置一次即可,多次设置无意义反而浪费性能
使用 MethodHandle 替代反射 高性能反射调用方式,支持 JIT 优化
使用动态代理(如 ByteBuddy) 生成字节码代理,性能几乎等同手写代码
控制反射调用频率 核心路径禁止频繁反射,比如高频请求、日志埋点等
基于注解构建编译期处理器 比如 Spring + AnnotationProcessor,编译期生成代码

# 六、常用工具类库

类库 作用说明
Spring ReflectionUtils 包装常用反射逻辑,简化代码编写
Apache BeanUtils Bean 属性映射、动态复制
ASM / ByteBuddy 生成字节码,实现零反射的高性能代理
MapStruct 静态代码生成器,避免反射
JDK MethodHandle 几乎等价于直接调用的反射方式