Java 反射:Inflation 机制让反射性能接近直接调用
一句话结论(30s)
反射慢在 Method.invoke() 的多重间接(MethodAccessor→Native JNI→安全检查→拆箱装箱),但因为 Inflation 机制前 15 次走 JNI、之后动态生成字节码,热路径性能可接近直接调用。关键设计是”冷启动 + 热路径”分离,默认阈值 15 平衡了”不要过早优化”与”热点极致优化”;代价是冷启动有 JNI 开销、热点需额外字节码生成 + JIT 预热。
核心原理(2min)
NativeMethodAccessorImpl 每次调用计数,超过 inflationThreshold(默认 15)即调 ReflectionFactory 用 ASM 在内存生成 GeneratedMethodAccessorXxx,其 invoke() 内直接写 invokevirtual 字节码可被 JIT 内联;setAccessible(true) 通过 copyMethod 置 override 标志,后续每次 invoke() 免去访问权限检查。
底层深入(5-10min)
反射为什么”慢”?
每个反射调用都经过多重间接:Method.invoke() → MethodAccessor.invoke() → Native JNI 调用 → 安全检查 → 拆箱装箱。这是反射”慢”的根本。
但 JDK 通过 Inflation 机制在热路径上把这种慢几乎消除。
💭 思考:反射慢,慢在哪一环?慢在
Method.invoke()要穿过 MethodAccessor、Native JNI、安全检查、拆箱装箱这么多层间接;每一层都吃一点,加起来就是几个数量级。优化思路自然就是「热路径把这些层减掉」。
Inflation 机制:前 15 次 Native,之后生成字节码
// NativeMethodAccessorImpl 源码简化
public Object invoke(Object obj, Object[] args) {
// 检查调用次数是否超过阈值
if (++numInvocations > ReflectionFactory.inflationThreshold()) {
// 超过阈值(默认 15)→ 动态生成 MethodAccessor
MethodAccessorImpl acc = (MethodAccessorImpl)
new ReflectionFactory().newMethodAccessor(method);
parent.setDelegate(acc); // 切换为"字节码版本"
return acc.invoke(obj, args);
}
// 前 15 次走 Native 调用
return invoke0(method, obj, args);
}
前 15 次反射调用走 JNI(免去字节码生成的开销),15 次后触发”膨胀”——ReflectionFactory 调用 ASM 在内存中生成 GeneratedMethodAccessorXxx 类。
这个生成的类的 invoke() 方法内直接写 invokevirtual 字节码调用目标方法,可以被 JIT 内联。 预热后性能接近直接调用。
💭 思考:为什么生成的字节码版本能接近直接调用?因为 Native 版本每次都要走 JNI 边界、做参数转换,而生成的 GeneratedMethodAccessor 的 invoke 里直接写 invokevirtual 字节码,JIT 一看这就是普通虚调用、可以内联——绕开了 JNI 这个最大开销源。
为什么阈值是 15?
冷启动优化——大多数反射只在框架启动时发生几次(注入依赖、解析配置),不需要字节码生成。只有真正的热路径(如循环中的反射调用)才值得生成字节码 + JIT 编译。15 是”只做一次就够”和”做多了浪费字节码生成”的折中。
💭 思考:阈值为什么是 15 而不是 3 或 100?这是个「不要过早优化」和「热点要极致优化」的折中:多数反射只调用几次(框架启动时注入),为它们生成字节码是浪费;真正热路径(循环里反复调)才值得花 ASM 生成 + JIT 预热。15 是经验值,不是数学最优。
反射的两个”安全开关”
method.setAccessible(true); // 绕过访问检查
setAccessible(true) 后,ReflectionFactory.copyMethod() 生成一个”root method”的副本,override 标志被置为 true。后续每次 invoke() 不再检查访问权限——省去安全检查的开销。频繁反射调用中调用 setAccessible(true) 是重要优化。
💭 思考:setAccessible(true) 为什么能提速?因为它让 copyMethod 置 override 标志,后续每次 invoke 跳过访问权限检查。权限检查在每次调用里都跑,省掉它等于省掉固定的一层开销——频繁反射调用里这是实打实的优化。
反射是 Spring IoC 和 MyBatis 的基石
Spring @Autowired 字段注入:
Field field = bean.getClass().getDeclaredField("userService");
field.setAccessible(true);
field.set(bean, userServiceProxy); // 写入依赖
MyBatis Mapper 代理:
// MapperProxy.invoke()
Method method = ...;
// 将接口方法调用转为 SQL 执行
return sqlSession.selectOne(method.getAnnotation(Select.class).value(), args);
没有反射,Spring 的依赖注入和 MyBatis 的接口代理都无法实现。
总结
反射的 Inflation 机制是”冷启动 + 热路径”的经典分离:冷路径走 JNI(免初始化),热路径动态生成字节码(可 JIT 内联)。15 次的阈值平衡了”不要过早优化”和”热点要极致优化”的工程权衡。
章末提问
追问 1:反射为什么慢?慢在哪些环节?
结论先行:慢在 Method.invoke() 的多重间接——MethodAccessor → Native JNI → 安全检查 → 拆箱装箱,每一层都有开销。
因为:直接调用只需一条 invokevirtual,而反射要走 Java 层 MethodAccessor 分发、跨 JNI 边界、做访问权限检查和参数数组的拆箱装箱,参数还要包装成 Object[]。这些固定成本叠加,使反射比直接调用慢几个数量级。
追问 2:Inflation 机制是怎么让反射热路径加速的?
结论先行:前 15 次走 Native JNI,超过阈值后 ReflectionFactory 用 ASM 在内存生成 GeneratedMethodAccessor,其 invoke 里直接写 invokevirtual 字节码,可被 JIT 内联。
因为:Native 版本每次调用都吃 JNI 边界开销;膨胀后生成的字节码版本把目标方法变成普通虚调用,JIT 可内联,预热后性能接近直接调用。核心是「冷启动走 JNI 免初始化、热路径生成字节码可内联」的分离。
追问 3:为什么反射调用前建议 setAccessible(true)?
结论先行:因为它把访问权限检查从「每次 invoke 都做」变成「只做一次」,省去热路径上的固定开销。
因为:setAccessible(true) 会让 ReflectionFactory.copyMethod 生成一个 override 标志为 true 的方法副本,后续 invoke 不再校验访问权限。对 Spring 依赖注入这类频繁反射场景,跳过每次安全检查是明显的性能优化。