深入理解java虚拟机笔记(6)虚拟机字节码执行引擎3

四、动态类型语言支持

Java虚拟机的字节码指令集的数量自从Sun公司的第一款Java虚拟机问世至今, 二十余年间只新增过一条指令, 它就是随着JDK 7的发布的字节码首位新成员——invokedynamic指令。 这条新增加的指令是JDK 7的项目目标: 实现动态类型语言(Dynamically Typed Language) 支持而进行的改进之一, 也是为JDK 8里可以顺利实现Lambda表达式而做的技术储备。 在本节中, 我们将详细了解动态语言支持这项特性出现的前因后果和它的意义与价值。

4.1 动态类型语言

在介绍Java虚拟机的动态类型语言支持之前, 我们要先弄明白动态类型语言是什么? 它与Java语言、 Java虚拟机有什么关系? 了解Java虚拟机提供动态类型语言支持的技术背景, 对理解这个语言特性是非常有必要的。

何谓动态类型语言? 动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的。

产生这种差别产生的根本原因是Java语言在编译期间却已将println(String)方法完整的符号引用(本 例中为一项CONSTANT_InterfaceMethodref_info常量) 生成出来, 并作为方法调用指令的参数存储到 Class文件中, 这个符号引用包含了该方法定义在哪个具体类型之中、 方法的名字以及参数顺序、 参数类型和方法返回值等信息, 通过这个符号引用, Java虚拟机就可以翻译出该方法的直接引用。

而ECMAScript等动态类型语言与Java有一个核心的差异就是变量obj本身并没有类型, 变量obj的值才具有类型, 所以编译器在编译时最多只能确定方法名称、 参数、 返回值这些信息, 而不会去确定方法所在的具体类型 (即方法接收者不固定) 。 “变量无类型而变量值才有类型”这个特点也是动态类型语言的一个核心特征。

静态类型语言能够在编译期确定变量类型, 最显著的好处是编译器可以提供全面严谨的类型检查, 这样与数据类型相关的潜在问题就能在编码时被及时发现, 利于稳定性及让项目容易达到更大的规模。 而动态类型语言在运行期才确定类型, 这可以为开发人员提供极大的灵活性, 某些在静态类型语言中要花大量臃肿代码来实现的功能, 由动态类型语言去做可能会很清晰简洁, 清晰简洁通常也就意味着开发效率的提升。

4.2 Java与动态类型

但遗憾的是Java虚拟机层面对动态类型语言的支持一直都还有所欠缺, 主要表现在方法调用方 面: JDK 7以前的字节码指令集中, 4条方法调用指令(invokevirtual、 invokespecial、 invokestatic、 invokeinterface) 的第一个参数都是被调用的方法的符号引用(CONSTANT_Methodref_info或者 CONSTANT_InterfaceMethodref_info常量) , 前面已经提到过, 方法的符号引用在编译时产生, 而动 态类型语言只有在运行期才能确定方法的接收者。

因此, 在Java虚拟机层面上提供动态类型的直接支持就成为Java平台发展必须解决的问题, 这便是JDK 7 时JSR-292提案中invokedynamic指令以及java.lang.invoke包出现的技术背景。

4.3 java.lang.invoke包

JDK 7时新加入的java.lang.invoke包是JSR 292的一个重要组成部分, 这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这条路之外, 提供一种新的动态确定目标方法的机制, 称为“方法句柄”(Method Handle) 。

确实, 仅站在Java语言的角度看, MethodHandle在使用方法和效果上与Reflection有众多相似之处。 不过, 它们也有以下这些区别: ·Reflection和MethodHandle机制本质上都是在模拟方法调用, 但是Reflection是在模拟Java代码层次的方法调用, 而MethodHandle是在模拟字节码层次的方法调用。 在MethodHandles.Lookup上的3个方法findStatic()、 findVirtual()、 findSpecial()正是为了对应于invokestatic、 invokevirtual(以及invokeinterface) 和invokespecial这几条字节码指令的执行权限校验行为, 而这些底层细节在使用Reflection API时是不需要关心的。 ·Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中java.lang.invoke.MethodHandle对象所包含的信息来得多。

前者是方法在Java端的全面映像, 包含了方法的签名、 描述符以及方法属性表中各种属性的Java端表示方式, 还包含执行权限等的运行期信息。 而后者仅包含执行该方法的相关信息。 用开发人员通俗的话来讲, Reflection是重量级, 而MethodHandle是轻量级。

·由于MethodHandle是对字节码的方法指令调用的模拟, 那理论上虚拟机在这方面做的各种优化(如方法内联) , 在MethodHandle上也应当可以采用类似思路去支持(但目前实现还在继续完善中) , 而通过反射去调用方法则几乎不可能直接去实施各类调用点优化措施。 MethodHandle与Reflection除了上面列举的区别外, 最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度看”之后: Reflection API的设计目标是只为Java语言服务的, 而MethodHandle则设计为可服务于所有Java虚拟机之上的语言, 其中也包括了Java语言而已, 而且Java在这里并不是主角。

4.4 invokedynamic指令 某种意义上可以说invokedynamic指令与MethodHandle机制的作用是一样的, 都是为了解决原有4条invoke*指令方法分派规则完全固化在虚拟机之中的问题, 把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中, 让用户(广义的用户, 包含其他程序语言的设计者) 有更高的自由度。

且, 它们两者的思路也是可类比的, 都是为了达成同一个目的, 只是一个用上层代码和API来实现,另一个用字节码和Class中其他属性、 常量来完成。 因此, 如果前面MethodHandle的例子看懂了, 相信读者理解invokedynamic指令并不困难。

每一处含有invokedynamic指令的位置都被称作“动态调用点(Dynamically-Computed Call Site) ”,这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量, 而是变为JDK 7时新加入的CONSTANT_InvokeDynamic_info常量, 从这个新常量中可以得到3项信息: 引导方法(Bootstrap Method, 该方法存放在新增的BootstrapMethods属性中) 、 方法类型(MethodType) 和名称。 引导方法是有固定的参数, 并且返回值规定是java.lang.invoke.CallSite对象, 这个对象代表了真正要执行的目标方法调用。 根据CONSTANT_InvokeDynamic_info常量中提供的信息, 虚拟机可以找到并且执行引导方法, 从而获得一个CallSite对象, 最终调用到要执行的目标方法上。

posted @ 2022-03-21 14:27  Mars.wang  阅读(30)  评论(0编辑  收藏  举报