反射调用慢的核心在于MethodAccessor的生成与跳转开销:首次调用走native路径,15次后触发膨胀生成JIT编译的GeneratedMethodAccessor,但始终存在虚分派、封箱解箱及安全检查;setAccessible(true)禁用膨胀机制,强制持续使用慢速native调用。

反射调用慢,核心在 MethodAccessor 的生成和跳转开销
Java 反射调用比直接调用慢几倍到几十倍,不是因为“反射本身慢”,而是 JVM 为每个 Method 对象懒加载并缓存一个 MethodAccessor 实例,而这个实例的底层实现会随调用次数动态切换——第一次是解释执行的 NativeMethodAccessorImpl,频繁调用后触发膨胀(inflation),生成并切换到 JIT 编译过的字节码版 GeneratedMethodAccessor。但无论哪种,都比静态绑定多出至少一次虚方法分派 + 参数封箱/解箱 + 安全检查。
-
Method.invoke()不是直接跳转到目标方法,而是先查MethodAccessor,再由它转发;每次调用都要走这套间接路径 - 首次调用必走 native 层(
NativeMethodAccessorImpl),开销最大;触发膨胀需满足sun.reflect.inflationThreshold(默认 15 次),之后才生成 Java 字节码版 - 膨胀后的
GeneratedMethodAccessor虽快,但类加载、字节码生成、JIT 编译都有延迟,且每个Method独占一个生成类,容易撑大 Metaspace - 如果目标方法是
private或需要绕过访问控制,还会额外触发Reflection.ensureMemberAccess(),做安全检查和修饰符校验
为什么 setAccessible(true) 有时反而更慢
很多人以为设成可访问就能“去掉检查就变快”,其实不然:对非 public 成员调用 setAccessible(true) 后,JVM 会禁用该成员的访问控制检查,但同时也会强制禁用反射膨胀机制——即永远不生成 GeneratedMethodAccessor,始终使用 NativeMethodAccessorImpl,也就是一直走 native 调用路径。
- 效果是:避免了安全检查,却锁死了最慢的调用路径
- 尤其在高频调用 private 方法时,
setAccessible(true)可能比不设还慢 2–3 倍 - 只有当方法本身调用频次极低(setAccessible(true)
- 替代方案:若可控,优先把方法改成
package-private或加public,让反射能走膨胀路径
如何实测判断是否已膨胀
不能只看调用耗时,得确认 MethodAccessor 类型。最直接的方式是反射读取 Method 内部字段,观察其 methodAccessor 实际类型。
Field maField = Method.class.getDeclaredField("methodAccessor");
maField.setAccessible(true);
Object accessor = maField.get(method);
System.out.println(accessor.getClass().getName());
- 输出
sun.reflect.NativeMethodAccessorImpl→ 还没膨胀 - 输出类似
sun.reflect.GeneratedMethodAccessor123→ 已膨胀,且数字越大说明越晚生成(可能已触发多次类加载) - 注意:JDK 9+ 模块系统下,
setAccessible(true)对Method.class字段可能失败,需用--add-opens java.base/sun.reflect=ALL-UNNAMED
真正有效的优化手段,不是避免反射,而是绕过它
想靠“缓存 Method 对象”或“预热调用”来提速,收益有限。关键是要减少反射调用频次,或用 JVM 更友好的替代路径。
立即学习“Java免费学习笔记(深入)”;
- 缓存
Method对象本身几乎没用——瓶颈不在查找,而在invoke()执行 - 用
MethodHandle替代Method.invoke():它支持直接链接(lookup.findVirtual()),跳过MethodAccessor层,性能接近直接调用(JDK 7+) - 对 Bean 属性操作,用
VarHandle(JDK 9+)或成熟的Objenesis+ 字节码生成库(如Byte Buddy)生成桥接方法,彻底脱离反射链 - Spring、Jackson 等框架内部早已不用原生反射,而是运行时生成代理类或使用
Unsafe/VarHandle,这才是高吞吐场景下的真实做法
反射慢不是 bug,是 JVM 为安全和通用性做的权衡。真正卡住性能的,往往不是第一次 invoke,而是你没意识到那个被反复调用的 Method 其实一直在走 native 路径,还误以为 setAccessible 就万事大吉。











