写点什么

详解 Java 方法句柄 MethodHandle

用户头像
林昱榕
关注
发布于: 1 小时前

方法句柄的概念


方法句柄是一种指向方法的强类型、可执行的引用,它的类型方法的参数类型返回值类型组成,而与方法所在类名和方法名无关。


它作为方法的抽象,使方法调用不再受所在类型的约束,能更好的支持动态类型语言特性。同时它也使得函数可以在 java 中成为一等公民,可以作为方法参数进行传递。


看个例子:

class Horse {  public void race() {    System.out.println("Horse.race()");   }}
class Deer { public void race() { System.out.println("Deer.race()"); }}
class Cobra { public void race() { System.out.println("How do you turn this on?"); }}
复制代码


如何用统一的方式调用 race 方法呢?


我们可以用反射,也可以抽象出一个包含 race 方法的接口,但这两种方式都不直接,要么有性能方面的损耗,要么增加了接口约束。


这个场景使用方法句柄就很合适,将 race 方法抽象成方法句柄,它们的句柄类型一致。这样调用者统一对接 race 方法的句柄,间接调用 race 方法。这其实跟使用接口统一交互界面是同样的道理,只不过方法句柄通过 MethodType 对象定义了方法的类型,不需要我们额外自定义类型。


方法句柄方式的参考实现:

public class RaceMethodHandleDemo {
public static void startRace(Object obj) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual(obj.getClass(), "race", MethodType.methodType(void.class)); mh.invoke(obj); }
public static void main(String[] args) throws Throwable { startRace(new Horse()); startRace(new Deer()); }}
复制代码


方法句柄类型示例


我们先练习几个小案例,一方面加深对句柄类型的理解,同时也体验下句柄的增删改操作。

import java.lang.invoke.MethodHandle;import java.lang.invoke.MethodHandles;import java.lang.invoke.MethodType;
public class MethodHandleTypeDemo {
public static void a(String s) { System.out.println("a: " + s); }
public void b(String s) { System.out.println("b: " + s); }
public static int c(String s1, String s2) { System.out.println("c: arg0: " + s1 + ", arg1:" + s2); return 1; }
public static void main(String[] args) throws Throwable { MethodHandles.Lookup l = MethodHandles.lookup(); MethodHandle mh0 = l.findStatic(MethodHandleTest.class, "a", MethodType.methodType(void.class, String.class)); System.out.println("mh0's type: " + mh0.type()); mh0.invokeExact("mh0");
MethodHandle mh1 = l.findVirtual(MethodHandleTest.class, "b", MethodType.methodType(void.class, String.class)); System.out.println("mh1's type: " + mh1.type()); mh1.invokeExact(new MethodHandleTest(), "mh1");
MethodHandle mh2 = mh1.bindTo(new MethodHandleTest()); System.out.println("mh2's type: " + mh2.type()); mh2.invokeExact("mh2");
MethodHandle mh3 = l.findStatic(MethodHandleTest.class, "c", MethodType.methodType(int.class, String.class, String.class)); System.out.println("mh3's type: " + mh3.type()); int c = (int) mh3.invokeExact("mh3-1", "mh3-2"); System.out.println("mh3 invokeExact result: " + c);
MethodHandle mh4 = MethodHandles.insertArguments(mh3, 0, "mh4-1"); System.out.println("mh4's type: " + mh4.type()); int c2 = (int) mh4.invokeExact("mh4-2"); System.out.println("mh4 invokeExact result: " + c2);
MethodHandle mh5 = MethodHandles.dropArguments(mh3, 1, String.class); System.out.println("mh5's type: " + mh5.type()); int c3 = (int) mh5.invokeExact("mh5-1", "mh5-2", "mh5-3"); System.out.println("mh5 invokeExact result: " + c3);
MethodHandle mh6 = mh0.asType(MethodType.methodType(void.class, Object.class)); System.out.println("mh6's type: " + mh6.type()); mh6.invokeExact((Object) "mh6"); }}
复制代码

执行结果如下:

mh0's type: (String)voida: mh0mh1's type: (MethodHandleTypeDemo,String)voidb: mh1mh2's type: (String)voidb: mh2mh3's type: (String,String)intc: arg0: mh3-1, arg1:mh3-2mh3 invokeExact result: 1mh4's type: (String)intc: arg0: mh4-1, arg1:mh4-2mh4 invokeExact result: 1mh5's type: (String,String,String)intc: arg0: mh5-1, arg1:mh5-3mh5 invokeExact result: 1mh6's type: (Object)voida: mh6
复制代码


根据打印结果,我们可以得到以下几点认识:

  • 实例方法的第一个参数是实例对象,也需包含在方法句柄类型中(对应 mh1);

  • 静态方法使用 MethodHandles.Lookup 的 findStatic 方法,实例方法使用 MethodHandles.Lookup 的 findVirtual,跟普通的方法调用字节码指令相对应(invokestatic 和 invokevirtual);

  • 通过 bindTo 绑定实例对象,产生的适配器句柄类型则不包含对象类型了(对比 mh1 和 mh2),这个方法在我们通过 invokedynamic 调用点链接到合法句柄类型时会使用到。

  • MethodHandles.insertArguments 方法可以在指定位置绑定一个参数,我们可以通过它实现方法的柯里化。比如可以将 f(x, y)方法的 x 参数绑定为 3,生成另一个方法句柄 g(y) = f(x, y),每当调用 g(y)时会先在第一个位置插入 3,再调用 f(x, y)的方法句柄。

  • MethodHandles.dropArguments 可以删除指定位置的参数,生成新的方法句柄,原理同 insertArguments。

  • MethodHandle.asType 则可以修改参数类型,为原方法句柄生成适配器。

方法句柄的权限


方法句柄也有权限问题,但与反射在方法调用时检查不同,它是在句柄创建阶段进行检查的,如果多次调用句柄,它比反射可以省下权限重复检查的开销。


但需注意的是,句柄的访问权限不取决于创建句柄的位置,而是 Lookup 对象的位置


实例演示:

import java.lang.invoke.MethodHandle;import java.lang.invoke.MethodHandles;
public class MethodHandleAuthorityTest {
public static void main(String[] args) throws Throwable { MethodHandles.Lookup l1 = Person.getLookup(); MethodHandle mh1 = l1.findGetter(Person.class, "name", String.class); System.out.println((String) mh1.invokeExact(new Person()));
MethodHandles.Lookup l2 = MethodHandles.lookup(); MethodHandle mh2 = l2.findGetter(Person.class, "name", String.class); System.out.println((String) mh2.invokeExact(new Person())); }}
class Person { private String name = "Mary";
public static MethodHandles.Lookup getLookup() { return MethodHandles.lookup(); }}
复制代码

结果如下:

MaryException in thread "main" java.lang.IllegalAccessException: member is private: com.mh.Person.name/java.lang.String/getField, from com.mh.MethodHandleAuthorityTest	at java.lang.invoke.MemberName.makeAccessException(MemberName.java:850)	at java.lang.invoke.MethodHandles$Lookup.checkAccess(MethodHandles.java:1536)	at java.lang.invoke.MethodHandles$Lookup.checkField(MethodHandles.java:1484)	at java.lang.invoke.MethodHandles$Lookup.getDirectFieldCommon(MethodHandles.java:1698)	at java.lang.invoke.MethodHandles$Lookup.getDirectField(MethodHandles.java:1688)	at java.lang.invoke.MethodHandles$Lookup.findGetter(MethodHandles.java:1027)	at com.mh.MethodHandleAuthorityTest.main(MethodHandleAuthorityTest.java:19)
复制代码


方法句柄的调用


句柄调用有两种方式:invokeExact invoke


invokeExact 会严格检查句柄类型跟传入参数类型,如果不一致就会报错。比如句柄类型要求 Object,你传的参数为 String,句柄调用时会报类型不匹配异常。这个情况下,需要将 String 显示强转成 Object 类型。


这里涉及到一个签名多态性(signature polymorphism)的概念(暂且认为签名等同于方法描述符)。

 public final native @PolymorphicSignature Object invokeExact(Object... args) throws Throwable;
复制代码


可以看到 invokeExact 方法有一个 @PolymorphicSignature 注解标识。java 虚拟机在处理被该注解标识的方法时,会根据实际参数类型生成方法描述符,而不是句柄目标方法声明的方法描述符。比如:

 public void test(MethodHandle mh, String s) throws Throwable {        mh.invokeExact(s);        mh.invokeExact((Object) s); }
// 该方法对应的字节码 public void test(java.lang.invoke.MethodHandle, java.lang.String) throws java.lang.Throwable; descriptor: (Ljava/lang/invoke/MethodHandle;Ljava/lang/String;)V flags: (0x0001) ACC_PUBLIC Code: stack=2, locals=3, args_size=3 0: aload_1 1: aload_2 2: invokevirtual #2 // Method java/lang/invoke/MethodHandle.invokeExact:(Ljava/lang/String;)V 5: aload_1 6: aload_2 7: invokevirtual #3 // Method java/lang/invoke/MethodHandle.invokeExact:(Ljava/lang/Object;)V 10: return
复制代码


当句柄调用 invokeExact 时会检查 invokevirtual 对应的方法描述符跟句柄的目标方法类型,如果不一致就会报类型不一致异常。


如果要自动适配参数类型,就需要调用 invoke 方法。invoke 方法会通过调用 MethodHandle.asType 方法生成一个句柄的适配器,该适配器会适配参数后再调用原方法句柄,同时对返回结果也会适配后再返回给用户。当然成功执行的前提是参数和返回结果都能够适配成功,即两个类型能够做类型转换。


方法句柄的实现


前面提到 jvm 会根据实际参数生成方法描述符,那这个描述符对应的方法是哪个呢?我们来实例验证一下。


在不加-XX:+ShowHiddenFrames 虚拟机参数之前,代码及执行结果如下:

import java.lang.invoke.*;
// -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames -Djava.lang.invoke.MethodHandle.DUMP_CLASS_FILES=truepublic class Foo { public static void bar(Object o) { new Exception().printStackTrace(); }
public static void main(String[] args) throws Throwable { MethodHandles.Lookup l = MethodHandles.lookup(); MethodType t = MethodType.methodType(void.class, Object.class); MethodHandle mh = l.findStatic(Foo.class, "bar", t); mh.invokeExact(new Object()); }}
// 输出结果java.lang.Exception at Foo.bar(Foo.java:13) at Foo.main(Foo.java:20)
复制代码


竟然是直接调用了目标方法?


且慢,在下结论之前,我们需要确保我们拿到的是真实的信息。


显然直接调用目标方法是不行的,因为需要做类型检查,必然需要在调用目标方法之前有所动作。所以显然是虚拟机隐藏了一些信息。


我们可以加上-XX:+ShowHiddenFrames 参数将隐藏的栈帧打印出来,这回结果如下:

java.lang.Exception	at Foo.bar(Foo.java:13)	at java.base/java.lang.invoke.DirectMethodHandle$Holder.invokeStatic(DirectMethodHandle$Holder:1000010)	at java.base/java.lang.invoke.LambdaForm$MH/0x0000000800b42840.invokeExact_MT(LambdaForm$MH:1000019)	at Foo.main(Foo.java:20)
复制代码


可以看到 invokeExact 方法调用了一个 LambdaForm 的方法,这是一个共享的、跟方法句柄类型相关的特殊适配器。我们可以通过-Djava.lang.invoke.MethodHandle.DUMP_CLASS_FILES=true 参数将它导出来:

final class LambdaForm$MH000 {    @Hidden    @Compiled    @ForceInline    static void invokeExact_MT000_LLL_V(Object var0, Object var1, Object var2) {        MethodHandle var3;        Invokers.checkExactType(var3 = (MethodHandle)var0, (MethodType)var2);        Invokers.checkCustomized(var3);        var3.invokeBasic(var1);    }
static void dummy() { String var10000 = "MH.invokeExact_MT000_LLL_V=Lambda(a0:L,a1:L,a2:L)=>{\n t3:V=Invokers.checkExactType(a0:L,a2:L);\n t4:V=Invokers.checkCustomized(a0:L);\n t5:V=MethodHandle.invokeBasic(a0:L,a1:L);void}"; }}
// 字节码static void invokeExact_MT000_LLL_V(java.lang.Object, java.lang.Object, java.lang.Object); descriptor: (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V flags: (0x0008) ACC_STATIC Code: stack=2, locals=3, args_size=3 0: aload_0 1: checkcast #14 // class java/lang/invoke/MethodHandle 4: dup 5: astore_0 6: aload_2 7: checkcast #16 // class java/lang/invoke/MethodType 10: invokestatic #22 // Method java/lang/invoke/Invokers.checkExactType:(Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)V 13: aload_0 14: invokestatic #26 // Method java/lang/invoke/Invokers.checkCustomized:(Ljava/lang/invoke/MethodHandle;)V 17: aload_0 18: aload_1 19: invokevirtual #30 // Method java/lang/invoke/MethodHandle.invokeBasic:(Ljava/lang/Object;)V 22: return
复制代码


可以看到,这个适配器会先调用 Invokers.checkExactType 方法检查参数类型,然后调用 Invokers.checkCustomized 方法。后者会在方法句柄调用多次后进行优化(对应参数-Djava.lang.invoke.MethodHandle.CUSTOMIZE_THRESHOLD,默认值为 127),最后调用方法句柄的 invokeBasic 方法。


invokeBasic 方法会调用至方法句柄本身持有的适配器中(也是一个 LambdaForm),我们可以通过反射将它打印出来:

Field field = MethodHandle.class.getDeclaredField("form");field.setAccessible(true);Object obj = field.get(mh);System.out.println(obj.toString());
复制代码


DMH.invokeStatic_L_V=Lambda(a0:L,a1:L)=>{    t2:L=DirectMethodHandle.internalMemberName(a0:L);    t3:V=MethodHandle.linkToStatic(a1:L,t2:L);void}
复制代码


该适配器会访问方法句柄中的 MemberName 对象,将该对象作为参数调用方法句柄的 linkToStatic 方法。该方法会根据 MemberName 对象中的方法地址或方法表索引跳转至目标方法。

final class MemberName implements Member, Cloneable {    ...    //@Injected JVM_Method* vmtarget;    //@Injected int         vmindex;    ...
复制代码


最后再补充说下 Invokers.checkCustomized 方法的作用。因为一开始为方法句柄生成的适配器是共享的,当方法句柄调用多次后,java 虚拟机会为它生成一个特有的适配器,该适配器将方法句柄当成常量,直接拿它的 MemberName 对象,然后调用 linkToStatic 方法。


我们可以将-Djava.lang.invoke.MethodHandle.CUSTOMIZE_THRESHOLD 设置为 0,此时生成的适配器就是特有的:

final class LambdaForm$DMH000 {    @Hidden    @Compiled    @ForceInline    static void invokeStatic000_LL_V(Object var0, Object var1) {        MethodHandle var3 = (MethodHandle)"CONSTANT_PLACEHOLDER_1 <<Foo.bar(Object)void/invokeStatic>>";        Object var2 = DirectMethodHandle.internalMemberName(var3);        MethodHandle.linkToStatic(var1, (MemberName)var2);    }
static void dummy() { String var10000 = "DMH.invokeStatic000_LL_V=Lambda(a0:L,a1:L)=>{\n t2:L=DirectMethodHandle.internalMemberName(a0:L);\n t3:V=MethodHandle.linkToStatic(a1:L,t2:L);void}"; }}
// 对应字节码static void invokeStatic000_LL_V(java.lang.Object, java.lang.Object); descriptor: (Ljava/lang/Object;Ljava/lang/Object;)V flags: (0x0008) ACC_STATIC Code: stack=2, locals=3, args_size=2 0: ldc #14 // String CONSTANT_PLACEHOLDER_1 <<Foo.bar(Object)void/invokeStatic>> 2: checkcast #16 // class java/lang/invoke/MethodHandle 5: astore_0 6: aload_0 7: invokestatic #22 // Method java/lang/invoke/DirectMethodHandle.internalMemberName:(Ljava/lang/Object;)Ljava/lang/Object; 10: astore_2 11: aload_1 12: aload_2 13: checkcast #24 // class java/lang/invoke/MemberName 16: invokestatic #28 // Method java/lang/invoke/MethodHandle.linkToStatic:(Ljava/lang/Object;Ljava/lang/invoke/MemberName;)V 19: return
复制代码


后续


方法句柄跟 invokedynamic 指令是什么关系?请听下回分解。


也欢迎关注我的公众号(搜索:Make IT Simple),一起学习交流。


 欢迎关注“Make IT Simple”,一起搞定底层原理

发布于: 1 小时前阅读数: 7
用户头像

林昱榕

关注

开心生活,努力工作。 2018.02.13 加入

还未添加个人简介

评论

发布
暂无评论
详解Java方法句柄MethodHandle