Method.invoke() 慢因每次调用需安全检查、类型转换、访问校验及类加载;MethodHandle 通过预编译句柄绕过反射包装层,减少开销,但需注意模块限制与签名适配。

为什么 Method.invoke() 慢得明显?
不是 JVM 优化不够,而是每次调用都要做安全检查、参数类型转换、访问控制校验,甚至触发类加载。尤其在高频调用场景(比如 JSON 反序列化、RPC 参数绑定),这部分开销会直接拖垮吞吐量。
- 默认
AccessibleObject.setAccessible(true)能跳过部分检查,但不解决参数装箱/反射解析开销 - Java 8+ 的
MethodHandle是更底层的调用机制,绕过了反射 API 的包装层,调用路径更短 - 注意:
MethodHandle不做运行时访问权限检查(除非显式启用安全管理器),这点和反射行为不同
怎么用 MethodHandle 替代 Method.invoke()?
核心是把反射调用“编译”成可复用的句柄,避免重复解析。关键步骤是获取、适配、缓存三步,缺一不可。
- 用
MethodHandles.lookup().findVirtual()或findSpecial()获取原始句柄(静态方法用findStatic()) - 如果目标方法参数含基本类型或需要自动装箱,必须用
MethodHandle.asType()显式适配签名,否则运行时报WrongMethodTypeException - 句柄本身线程安全,可全局缓存;但不要缓存
lookup实例——它带访问上下文,复用可能触发IllegalAccessException
示例:获取并缓存一个 setter
private static final MethodHandle SETTER;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
SETTER = lookup.findVirtual(Person.class, "setName",
MethodType.methodType(void.class, String.class))
.asType(MethodType.methodType(void.class, Person.class, String.class));
} catch (Throwable t) {
throw new ExceptionInInitializerError(t);
}
}
反射缓存自己实现要注意什么?
很多人用 ConcurrentHashMap<method method></method> 存 setAccessible(true) 后的 Method,这只能省一次检查,不能省参数转换。真正值得缓存的是调用入口,不是反射对象本身。
立即学习“Java免费学习笔记(深入)”;
- 缓存键建议用
Class + method name + parameter types组合,别只用Method.toString()(重载时会冲突) - 避免缓存未校验访问权限的
Method:子类重写后,父类缓存的Method可能无法访问 - 如果使用 Spring 等框架,优先走它们的
ReflectionUtils或ResolvableType工具类——它们内部已做了签名归一化和缓存
MethodHandle 和反射在 JDK 版本上的坑
JDK 9+ 引入模块系统后,MethodHandles.lookup() 默认无法跨模块访问非导出类成员。即使加了 --add-opens,某些场景下 findSpecial() 仍会失败。
- JDK 8 下
MethodHandle性能优势最稳定;JDK 17+ 因 JIT 优化增强,普通反射差距缩小,但高频小方法调用仍推荐MethodHandle -
VarHandle(JDK 9+)比MethodHandle更轻量,适合字段读写,但不适用于任意方法调用 - 别在
toString()、hashCode()这类被 JIT 频繁内联的方法里用反射或MethodHandle——容易让 JIT 放弃优化整个方法
缓存逻辑越靠近业务入口越好,但 MethodHandle 初始化失败时的兜底策略必须明确,不能静默退化为慢路径。











