invokedynamic的指鹿为马是如何实现的?
鸭子呱呱叫
在Go语言中, 我们有鸭子类型的这一种说法, 也就是看起来像马的, 都是可以被当做马的。
1 | type House interface { |
JVM底层是如何做的呢?Java引入一个专门的指令叫做invokedynamic, 该指令的调用机制抽象出了调用点这个概念, 并允许将调用点链接到任意符合条件的方法上
作为invokedynamic的准备工作, Java7还引入了更加底层的抽象: 方法句柄, 方法句柄是一个强类型的, 能够直接执行的引用。
1 | handle -->static method/object method |
当指向字段的时候, 方法句柄实际上指向包含字段访问字节码的虚构方法, 语义上等价于目标字段的getter或者setter方法
方法句柄的类型是由所指向的是方法签名, 但是我们并不关系方法所属的类名或者方法名
方法句柄是MethodHandles.lookup
类来完成的, 它提供多个API
1 | class Foo{ |
注意方法句柄的访问权限不取决于方法句柄的创建位置, 而是取决于Lookup对象的创建位置, 如果一个lookup对象是在私有字段所在的类进行获取的, 那么这个lookup对象便拥有对该字段的访问权限
但是需要注意的是: 方法句柄在运行时没有权限检查, 所以当某个方法句柄指向了一个私有方法, 那么就会导致访问权限被泄露出去。
方法句柄的操作
方法句柄的调用分为两种, 一种是严格匹配的类型, 比如一个方法句柄的对应的方法描述符是Object类型, 那么你就需要调用对应的
1 | public void test(Method mh, String s) { |
invokeExact
会确认该 invokevirtual
指令对应的方法描述符,和该方法句柄的类型是否严格匹配。在不匹配的情况下,便会在运行时抛出异常。
第二种方式是使用invoke方法, 这种方法的好处就是可以帮助我们适配对应的方法。
方法句柄的底层
1 | import java.lang.invoke.*; |
首先我们将上面这个代码给编译一下, 你会发现打印出来的栈信息只有main函数和bar函数, 这也就说明虚拟机隐藏了一部分信息, 我们可以通过下面这个命令给它打印出来
1 | java -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames Foo |
对应的输出是:
1 | at Foo.bar(Foo.java:5) |
然后我们将这隐藏的这部分的信息给导出对应的class文件
1 | -Djava.lang.invoke.MethodHandle.DUMP_CLASS_FILES=true |
通过导出的class文件, 我们可以知道, 它会调用对应的适配器
- 首先会调用
checkExtractType
方法 - 其次会调用
invokers.checkCustomized
方法, 这个方法的作用是在超过一个阈值的情况下进行优化 - 最后它会调用方法句柄的
invokeBasic
方法, JVM也会对这个指令进行优化这会将它调用到方法句柄本身所持有的适配器中
1 | // 该方法句柄持有的LambdaForm实例的toString()结果 |
- 这个适配器将获取方法句柄中的
MemberName
类型的字段,并且以它为参数调用linkToStatic
方法。估计你已经猜到了,Java 虚拟机也会对linkToStatic
调用做特殊处理,它将根据传入的MemberName
参数所存储的方法地址或者方法表索引,直接跳转至目标方法。
invokers.checkCustomized
的优化是这样的, 方法句柄一开始持有的适配器是共享的。当它被多次调用之后,Invokers.checkCustomized
方法会为该方法句柄生成一个特有的适配器。这个特有的适配器会将方法句柄作为常量,直接获取其 MemberName
类型的字段,并继续后面的 linkToStatic
调用。