Java中,方法调用会被编译为invokestatic、invokespecial、invokevirtual、invokeinterfaces四种指令。这些指令与包含目标方法类名、方法名以及方法描述符的符号引用捆绑。在实际运行之前,JVM将根据这个符号引用链接到具体的目标方法。
这四种调用指令中,JVM明确要求方法调用需要提供目标方法的类名:
- 调用其中一种类型的目标方法。不符合的类型,套一层马甲后再调用。
- 通过反射机制,来查找并且调用各个类型中的目标方法,以此来模拟真正的目标方法。
Java7引入了一条新的指令invokedynamic,该指令的调用机制抽象出调用点这一个概念,并允许应用程序将调用点链接至任意符合条件的方法上(加载一个任意对象,invokedynamic调用目标方法)。
作为invokedynamic的准备工作,Java7引入了更加底层、更加灵活的方法抽象:方法句柄(MethodHandle)。
方法句柄的概念
方法句柄是一个强类型的,能够被直接执行的引用。该引用可以指向常规的静态方法或者实例方法,也可以指向构造器或者字段。当指向字段时,方法句柄实则指向包含字段访问字节码的虚构方法,语义上等价于目标字段的getter或者setter方法(但并不会直接指向目标字段所在类中的getter/setter,因为已有的getter/setter方法不一定就是在访问目标字段)。
方法句柄的类型(MethodType)是由所指向方法的参数类型以及返回类型组成的。它是用来确认方法句柄是否适配的唯一关键。当使用方法句柄时,并不关心方法句柄所指向方法的类名或者方法名。
方法句柄的创建是通过MethodHandles.Lookup类来完成的。它提供了多个API,既可以使用反射API中的Method来查找,也可以根据类、方法名以及方法句柄类型来查找(需区分具体的调用类型)。
- Lookup.findStatic方法-invokestatic调用的静态方法。
- Lookup.findVirtual方法-invokevirtual调用的实例方法&invokeinterface调用的接口方法。
- Lookup.findSpecial方法-invokespecial调用的实例方法。
调用方法句柄,和原本对应的调用指令时一致的。对于原本用invokevirtual调用的方法句柄,它也会采用动态绑定;而对于原本用invokespecial调用的方法句柄,它会采用静态绑定。
方法句柄同样也有权限问题。但它与反射API不同,其权限检查是在句柄的创建阶段完成的。在实际调用过程中,JVM并不会检查方法句柄的权限。如果该句柄被多次调用,与反射调用相比,它将省下重复权限检查的开销。
由于方法句柄没有运行时权限检查,应用程序需要负责方法句柄的管理。一旦它发布了某些指向私有方法的方法句柄,那么这些私有方法便被暴露出去了。
方法句柄的访问权限不取决于方法句柄的创建位置,而是取决于Lookup对象的创建位置。
对于一个私有字段,如果Lookup对象是在私有字段所在类中获取的,那么这个Lookup对象便拥有对该私有字段的访问权限,即使是在所在类的外边,也能够通过该Lookup对象创建该私有字段的getter或者setter。
1 | import java.lang.invoke.*; |
方法句柄的操作
方法句柄的调用可分为两种,一是需要严格匹配参数类型的invokeExact。而是自动适配参数类型的invoke。
invokeExact
假设一个方法句柄将接收一个Object类型的参数,如果直接传入String作为实际参数,那么方法句柄的调用会在运行时抛出方法类型不匹配的异常。正确的调用方式是将该String显示转化为Object类型。
在普通Java方法调用中,只有在选择重载方法时,才会用到显式转化。经过显式转化后,参数的声明类型发生了改变,因此有可能匹配到不同的方法描述符,从而选取不同的目标方法。
方法句柄API有一个特殊的注解类@PolymorphicSignature(签名多态性,signature polymorphism)。在碰到被它注解的方法调用时,Java编译器会根据所传入参数的声明类型来生成方法描述符,而不是采用目标方法所声明的描述符。
public final native @PolymorphicSignature Object invokeExact(Object… args) throws Throwable;
invokeExact会确认invokevirtual(以及其他三种)指令对应的方法描述符,和该方法句柄的类型是否严格匹配。在不匹配的情况下,便会在运行时抛出异常。
invoke
invoke同样是一个签名多态性的方法。invoke会调用MethodHandle.asType方法,生成一个适配器方法句柄,对传入的参数进行适配,再调用原方法句柄。调用原方法句柄的返回值同样也会先进行适配,然后再返回给调用者。
方法句柄还支持增删改参数的操作,这些操作都是通过生成另一个方法句柄来实现的。
- 改操作,通过MethodHandle.asType方法实现。
- 删操作,通过MethodHandles.dropArguments方法,将传入的部分参数就地抛弃,再调用另一个方法句柄。
- 增操作,通过MethodHandle.bindTo方法,往传入的参数中插入额外的参数,再调用另外一个方法句柄。
Java8 中捕获类型的Lambda表达式便是使用这种操作来实现的。
增操作还可以用来实现方法的柯里化。
方法句柄的实现
HotSpot虚拟机中方法句柄的具体实现(DirectMethodHandle为例)。
通过查看栈轨迹,invokeExact的目标方法就是方法句柄指向的方法。
启用-XX:+ShowHiddenFrames参数打印被JVM隐藏的栈信息。
1 | $ java -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames Foo |
JVM会对invokeExact调用做特殊处理,调用至一个共享的、与方法句柄类型相关的特殊适配器中。这个适配器是一个LambdaForm,可以通过添加虚拟机参数将之导出成class文件(-DJava.lang.invoke.MethodHandle.DUMP_CLASS_FILES=true)。
1 | final class java.lang.invoke.LambdaForm$MH000 { static void invokeExact_MT000_LLLLV(jeava.lang.bject, jjava.lang.bject, jjava.lang.bject); |
在这个适配器中,它会调用Invokers.checkExactType方法来检查参数类型,然后调用Invokers.checkCustomized方法,在方法句柄的执行次数超过一个阈值时进行优化(Djava.lang.invoke.MethodHandle.CUSTOMIZE_THRESHOLD=127),最后调用方法句柄的invokeBase方法。
JVM同样会对invokeBasic调用做特殊处理,将调用至方法句柄本身所持有的适配器中,这个适配器同样是一个LambdaForm,通过反射机制将其打印出来。
1 | // 该方法句柄持有的LambdaForm实例的toString()结果 |
这个适配器将获取方法句柄中的MemberName类型的字段,并且以它为参数调用linkToStatic方法。
JVM也会对linkToStatic调用做特殊处理,它将根据传入的MemberName参数所存储的方法地址或者方法表索引,直接跳转至目标方法。
1 | final class MemberName implements Member, Cloneable { |
方法句柄一开始持有的适配器是共享的。当它被多次调用之后,Invokers.checkCustomized方法会为该方法句柄生成一个特有的适配器。这个特有的适配器会将方法句柄作为常量,直接获取其MemberName类型的字段,并继续后面的linkToStatic调用。
1 |
|
方法句柄的调用和反射调用一样,都是间接调用。因此它也面临无法内联的问题。与反射不同的是,方法句柄的内联瓶颈在于即时编译器能否将该方法句柄识别为常量。
总结
方法句柄是invokedynamic底层机制的基石。
方法句柄是一个强类型、能够被直接执行的引用。它仅关心所指向方法的参数类型以及返回类型,而不关心方法所在的类以及方法名。方法句柄的权限检查发生在创建过程中,相较于反射调用节省了调用时反复权限检查的开销。
方法句柄可以通过invokeExact以及invoke来调用。其中invokeExact要求传入的参数和所指向方法的描述符严格匹配。方法句柄还支持增删改参数的操作,这些操作是通过生成另一个充当适配器的方法句柄来实现的。
方法句柄和反射调用一样,都是间接调用,同样会面临无法内联的问题。