原始博客的地址:
该项目的地址:
上一篇我们实现了方法调用以及参数传递,主要讨论了四个invoke指令。但是有一条指令我们没有讨论,那就是在JDK 7里面新增的invokedynamic指令。
这条新增加指令的目标:实现动态类型语言(Dynamically Typed Language)支持而进行的改进之一,也是为JDK 8里可以顺利实现Lambda表达式而做的技术储备。

动态类型语言

我们先简单的讨论一下动态类型语言是什么?看一个例子:
现在先假设这行代码是在Java语言中,并且变量obj的静态类型为java.io.PrintStream,那变量obj的实际类型就必须是PrintStream的子类(实现了PrintStream接口的类)才是合法的。否则,哪怕obj属于一个确实包含有println(String)方法相同签名方法的类型,但只要它与PrintStream接口没有继承关系,代码依然不可能运行——因为类型检查不合法。
但是相同的代码在ECMAScript(JavaScript)中情况则不一样,无论obj具体是何种类型,无论其继承关系如何,只要这种类型的方法定义中确实包含有println(String)方法,能够找到相同签名的方法,调用便可成功。
产生这种差别产生的根本原因是Java语言在编译期间却已将println(String)方法完整的符号引用(本例中为一项CONSTANT_InterfaceMethodref_info常量)生成出来,并作为方法调用指令的参数存储到Class文件中,例如下面这个样子:
这个符号引用包含了该方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,Java虚拟机就可以翻译出该方法的直接引用。
而ECMAScript等动态类型语言与Java有一个核心的差异就是变量obj本身并没有类型,变量obj的值才具有类型,所以编译器在编译时最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的具体类型。

MethodHandle

JDK 7新加入了一个java.lang.invoke包,这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这条路之外,提供一种新的动态确定目标方法的机制,称为“方法句柄”(Method Handle)。
举个例子,如果我们要实现一个排序函数,在C/C++中的常用做法是用函数指针,像这样:
但在Java语言中做不到这一点,没有办法单独把一个函数作为参数进行传递(但是 kotlin 可以做到,kotlin 也可以运行在JVM上,那么它是如何运行起来的呢?)。普遍的做法是设计一个带有compare()方法的Comparator接口,以实现这个接口的对象作为参数,例如Java类库中的Collections::sort()方法就是这样定义的:
有了 MethodHandle 之后,我们就可以拥有类似于函数指针这样的工具了。看一个例子:
执行 main 方法,可以获得输出:
MethodHandle看起来和反射很像,但是实际上它比反射要轻量级一些。因为Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。有兴趣的可以去看一些原理分析:
简单理解就是,针对 MethodHandleinvoke 方法,会有特殊的逻辑,最终会模拟出对象上方法调用的效果。

invokedynamic

由于invokedynamic指令面向的主要服务对象并非Java语言,而是其他Java虚拟机之上的其他动态类型语言,因此,光靠Java语言的编译器Javac的话,在JDK 7时甚至还完全没有办法生成带有invokedynamic指令的字节码(曾经有一个java.dyn.InvokeDynamic的语法糖可以实现,但后来被取消了),而到JDK 8引入了Lambda表达式和接口默认方法后,Java语言才算享受到了一点invokedynamic指令的好处。所以我们下面就以 lambda举例,但是可能会比较别扭,因为 lambda 的例子基本与 MethodHandle 看不出来啥关系。但是从某种意义上来说invokedynamic指令与MethodHandle机制的作用是一样的。
看下面的例子,这个例子可以体会一下 invokedynmic 的神奇之处:
通常,我们会认为 runnable 赋值的那一行代码是匿名内部类的一个语法糖写法。但是实际上,我们打开字节码看一看会发现并不是:
notion image
可以看到上面并没有匿名内部类相关的字节码。那么 runnable 对象的 run 方法是如何被执行的呢?
在第8个字节的位置,出现了一个 invokedynamic 指令。我们看一下官方文档:
notion image
可以看到,它后面跟了4个字节的操作数,前两个字节组成常量池的索引,后两个字节永远为0。
常量池索引指向的是一个 CONSTANT_InvokeDynamic_info 类型的常量。而这个常量里面又指向了 BootstrapMethods的索引:
notion image
BootstrapMethods 是类的一个属性:
notion image
可以看到,这个BootstrapMethods 属性里面储存了一个 Bootstrap 方法的列表。它指向了 lambda$main$0 方法,但是我们写的代码中并没有这个方法呀?这是因为这个方法是动态生成的。
我们dump一下生成的 lambda 类,在测试类上添加如下代码:
重新运行 main 方法,发现生成的类如下:
看到这个类,我们就知道,invokedynamic 指令会返回这个InvokeDynamicTest$$Lambda$1对象,然后 invokeinterface 也是执行的这个对象的方法。打断点发现我们的猜测是对的:
notion image
至于,invokedynamic 是如何获取到InvokeDynamicTest$$Lambda$1 对象的,就不深入了。从 BootStrap里面的描述来看,它也是使用了MethodHandle 来做的。
 

实现 invokedynamic 指令

我们已经对 invokedynamic 指令有了一定的了解,现在我们需要实现该指令,以便让我们的JVM支持一种很简单的 lambda 表达式写法:
我们需要让上面的代码能够正常运行,这个 lambda 表达式非常的简单,写起来并不难。虽然也可以支持更加复杂的行为,比如外部参数使用等等,但是逻辑写起来就过于复杂,不利于学习了。
首先,我们看看 invokedynamic 指令结构:
notion image
它的操作数分为两部分,第一部分是指向常量池的索引,第二部分固定为0。代码解析如下:
索引对应的常量池的类型是一个 CONSTANT_InvokeDynamic 类型:
notion image
它的结构体里面储存了2个信息,第一个是 lambda 表达式实现的接口方法名和类型。第二个是一个 BootstrapMethods 的索引。
每一个动态调用点(暂时简单理解为 lambda 表达式),都对应着一个 BootstrpMethod:
notion image
如果我在程序中写两个 lambda 表达式,BootstrapMethods 就会有两项。BootstrapMethod 的 参数里面的第二项就储存了它真正需要调用的方法的描述信息。有了它,我们就可以实现 lambda 表达式。
思路就是:
  1. 创建一个 MyClass 对象 LambdaClass,给它生成一个规则类名。
  1. 找到真正要调用的方法,将这个方法 copy 一份,放到 LamdaClass 里面并与之关联起来。
  1. 执行 invokedynamic 指令时,返回一个 LambdaClass 类创建的对象。
  1. 完成,后面的流程,就是 invokeinterface,由于我们给类塞了一个方法,所以会自动的寻找到我们塞的方法并执行。
notion image
具体逻辑如下:
题外话,这样的写法是非常取巧的,因为我们真正执行的是一个编译器生成的 static 的方法,而 static 的方法在参数传递的时候会少一个 this 参数,但是在 invokeinterface 的时候是需要传递 this 的,所以会导致操作数栈各种问题。
我们没有实现自己的前端编译器,所以没法生成一个 class 文件,从而只能借用现有的信息做一些hack工作,最后达到目的。
最终的目的是让自己熟悉一下 invokedymic 指令,以及 lambda 表达式的奥秘。

测试

配置:
notion image
输出:
notion image
完美!!!
Loading...
二手的程序员
二手的程序员
路漫漫其修远兮
公告
 
可以占个坑,虽然现在没人说话。
QQ群:
notion image
Telegram群:
notion image
 
我的公众号:
notion image
我的微信:
notion image