methodhandle 是 jvm 底层更贴近字节码的调用机制,比反射快在 jit 可内联优化、无每次安全检查与参数包装开销;需用 methodhandles.lookup() 获取句柄,严格类型匹配(invokeexact),建议缓存复用。

MethodHandle 是什么,和反射比快在哪
它不是反射的“升级版”,而是 JVM 底层提供的、更贴近字节码指令的调用机制。MethodHandle 的核心优势是:一旦解析完成(即获取到句柄),后续调用可被 JIT 编译器内联优化,而 Method.invoke() 每次都走完整反射路径,带安全检查、参数包装、异常转换等开销。
常见错误现象:MethodHandle.invokeExact() 报 WrongMethodTypeException —— 这不是 bug,是设计使然:它要求参数类型和返回值类型必须完全匹配,连 int 和 Integer 都不自动装箱。
使用场景:
- 高频调用(比如 JSON 反序列化框架里反复 set 字段)
- 动态语言运行时(如 Nashorn、GraalVM 的 Truffle 实现)
- 替代
Unsafe.defineAnonymousClass+ 手写字节码的轻量方案
性能影响明显:在 HotSpot 上,warmup 后 invokeExact() 接近直接方法调用;但首次解析(MethodHandles.lookup().findVirtual())比反射略慢,因为要校验访问权限并生成适配器。
立即学习“Java免费学习笔记(深入)”;
怎么从类和方法名拿到 MethodHandle
关键入口永远是 MethodHandles.lookup(),它代表当前类的访问上下文。不能随便 new,必须用这个静态工厂方法获取。
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(
String.class,
"length",
MethodType.methodType(int.class)
);
注意三点:
-
findVirtual()用于实例方法,findStatic()用于静态方法,findSpecial()用于 super 调用或私有实例方法 -
MethodType.methodType()的第一个参数是返回类型,后面才是参数类型,顺序和构造函数签名一致,别反了 - 如果目标方法是私有的,且不在当前类中,
lookup默认无权访问 —— 需要提前用lookup.in(TargetClass.class)切换上下文,或者确保目标类对当前类可见(比如同包、public)
容易踩的坑:lookup.findVirtual(Obj.class, "get", mt) 中 Obj.class 必须是实际声明该方法的类,不能是子类(除非子类重写了)。否则抛 NoSuchMethodException。
invokeExact() 和 invoke() 的区别必须分清
invokeExact() 是严格模式:传参类型、个数、返回类型必须和 MethodHandle.type() 完全一致。invoke()(已过时,JDK 15+ 移除)曾尝试自动适配,但语义模糊,现在统一用 invokeExact() + 显式适配。
常见错误现象:把 String::length 的句柄拿来传 "" 以外的类型,或者漏掉 throws Throwable 声明,编译就报错。
实操建议:
- 先打印mh.type() 看签名,再写调用代码
- 遇到类型不匹配,用 mh.asType(MethodType.fromMethodDescriptorString("(Ljava/lang/String;)I", null)) 转换,但注意这会生成新句柄,有开销
- 若需处理 checked exception,别指望 invokeExact() 自动包装 —— 它原样抛出,你得自己 try-catch
缓存 MethodHandle 比重复查找更重要
每次调用 lookup.findXXX() 都触发解析和权限检查,开销不小。而 MethodHandle 对象本身是线程安全、可复用的。
正确做法是:在类初始化期(static block)、Spring Bean 初始化时、或第一次访问时懒加载并缓存。
private static final MethodHandle STRING_LENGTH;
static {
try {
STRING_LENGTH = MethodHandles.lookup()
.findVirtual(String.class, "length", MethodType.methodType(int.class));
} catch (Throwable t) {
throw new ExceptionInInitializerError(t);
}
}
容易忽略的点:缓存的是句柄,不是 lookup 实例;lookup 只用来“造句柄”,造完就扔。很多人误以为要长期持有 lookup,其实完全没必要。
缓存后,高频调用只需 STRING_LENGTH.invokeExact("abc"),无反射开销,也无需 setAccessible(true)。
复杂点在于:如果目标方法可能被重定义(比如 JVMTI agent 修改字节码),句柄不会自动失效,仍指向原始版本 —— 这既是优点(稳定),也是陷阱(热更新场景需重新 lookup)。










