反射调用比直接调用慢10–100倍,因JVM无法内联Method.invoke(),需每次动态解析、权限检查、参数装箱拆箱、异常包装,并绕过JIT优化路径。

反射调用在频繁场景下比直接调用慢出数量级,根本不在“写法不同”,而在JVM和CLR底层执行路径的结构性差异——它绕过了编译期确定的一切优化前提,被迫在每次调用时重复完成本该只做一次的工作。
运行时解析无法跳过:每次调用都要重走查找链
直接调用是静态绑定:字节码里直接存着目标方法的符号引用,JIT编译后变成硬编码的内存地址跳转。而反射调用必须每次执行:
- 从
Method对象中取出methodAccessor实例(首次为NativeMethodAccessorImpl) - 检查该
Method是否被setAccessible(true)禁用膨胀,决定走native还是生成字节码路径 - 对参数数组做封箱/解箱(
Object[]→ 基本类型值 → 方法实际参数) - 触发
Reflection.ensureMemberAccess()做修饰符与安全管理校验(哪怕已设setAccessible(true),校验逻辑仍存在)
JIT优化全面失效:没有内联,没有常量传播,没有逃逸分析
JIT编译器依赖稳定调用模式识别热点代码。反射调用因目标方法在运行时才确定,导致:
- 无法内联:JIT看不到具体方法体,只能保留
Method.invoke()这个间接入口 - 参数对象无法栈上分配:传入的
Object[] args必在堆上创建,高频调用直接推高GC压力 - 调用链不可预测:同一行
method.invoke(obj, args)下次可能指向完全不同类的方法,JIT放弃推测性优化
访问控制与安全机制带来固定开销
即使对public方法,反射也要走完整安全检查流程;对private方法,setAccessible(true)看似“提速”,实则锁死最慢路径:
- 禁用膨胀机制 → 永远不生成
GeneratedMethodAccessor→ 持续走JNI跨语言调用 - 每次调用都触发
java.lang.SecurityManager(若启用)或AccessControlContext上下文校验 - 字段/方法访问需反复解析修饰符位(
Modifier.PRIVATE等),而非编译期常量折叠
缓存与间接层叠加放大延迟
表面上看,Method对象可复用,但真正耗时的是它的委托载体MethodAccessor:
- 每个
Method独占一个MethodAccessor实例,且GeneratedMethodAccessor是动态生成的类,加载它要进Metaspace、触发类验证、等待JIT编译就绪 - 调用过程是“
Method→MethodAccessor→ 目标方法”,至少两次虚方法分派(vtable lookup) - DLR(C# dynamic)或
invokedynamic(Java)虽有CallSite缓存,但首次绑定仍需完整解析,且缓存命中率受类型稳定性制约










