Java7一项重要的改进是JVM中增加了invokedynamic指令,用于支持非Java语言,尤其是动态语言。本文翻译自官方的Java

Virtual Machine Support for Non-Java

Languages 。

名词中英对照

invokedynamic instructions

动态调用指令 或 invokedynamic指令

method handle

方法句柄,是CallSite的目标(target)

bootstrap method

引导方法

CallSite

调用点

method type

方法类型

specifier

说明符

介绍

Java平台是一个很优秀的基础平台(略)。

Java 7平台使非Java语言能够利用JVM的基础设施和潜在的性能优化,关键机制是invokedynamic指令。

它简化了JVM上动态类型语言的编译器和运行时系统的实现。

静态类型与动态类型

在编译时进行类型检查的语言是静态类型语言。类型检查是验证一个程序类型安全的过程。如果程序所有操作的参数都是正确的类型,那么这个程序是类型安全的。

Java是一种静态类型语言。类和实例变量,方法参数,返回值和其它变量的所有类型信息,在编译时都是已知的。Java语言的编译器使用这些类型信息产生强类型的字节码,然后可以由JVM在运行时高效执行。

这个Hello World展示了静态类型例子。类型显示为粗体。

invoke函数 java_常量池

在运行时进行类型检查的语言是动态类型语言。JavaScript和Ruby都是动态类型语言的例子。这些语言是在运行时验证应用中的这些值是否符合期望的类型,而不是在编译时。这些语言通常在编译时没有任何类型信息可用。对象的类型信息仅在运行时才能决定。因此,过去很难高效地在JVM上实现他们。

下面是一个Ruby语言的Hello World示例:

invoke函数 java_常量池

注意每个名字都没有类型声明。main程序也不在一个持有类型里(Java的HelloWorld类)。相对于Java的for循环,Ruby是在动态类型的变量ARGV中。循环体被包含在一个闭包块中,闭包是动态语言常有的特性。

静态类型语言不一定是强类型的语言

强类型语言对其要操作的值的类型有特定的限制。如果计算机语言实现了强类型,那么它可以防止参数类型不对的操作执行。相反,弱类型的语言会隐式地对错误或不兼容类型的操作参数进行转换。

静态类型语言既可以是强类型的也可以是弱类型的。类似的,动态类型语言也可以是强类型或弱类型的。例如,Ruby语言是动态类型的也是强类型的。一旦变量被某种类型的值初始化后,Ruby语言将不会隐式地把这个变量转换为其它的数据类型。Ruby语言不允许这样:

invoke函数 java_常量池

在这个例子中,Ruby不会把数字2(Fixnum类型)隐式地转换为字符串。

编译动态类型语言的挑战

考虑下面这个动态类型方法addtwo ,它把两个数字(可以是任何数字类型)相加并返回和:

invoke函数 java_常量池

假设你要实现一个编程语言的编译器和运行时系统,其中写了这个 addtwo

方法。对于强类型语言而言,无论是静态类型还是动态类型,+(加操作符)的行为取决于操作数的类型。静态类型语言的编译器要基于a和b的静态类型来选择使用+的哪种实现合适。例如,如果a和b都是int型那么Java编译器使用iadd

JVM指令来实现+。加操作符将被编译到一个方法调用中,因为JVM的iadd指令要求静态已知操作数的类型。

相反,动态类型语言的编译器必须延迟到运行时再做出选择。a + b 语句被编译为 +(a, b) 这样

的方法调用,其中+是方法名。(注意+作为方法名在JVM中是允许的但在Java语言中不允许)。然后假设动态类型语言的运行时系统能够识别出a和b都是整数类型的变量。运行时系统会优先选择调用针对整数类型的+方法实现,而不是针对任意对象类型的实现。

编译动态类型语言的挑战是如何实现一个运行时系统,在程序被编译之后能够选出最合适的方法或函数的实现。把所有变量都当作Object类的对象不会很高效;Object类不包含叫作+的方法。

Java

7引入了invokedynamic指令,使运行时系统能够自定义调用点和方法实现之间的连接。在这个例子中,动态调用的调用点是+。动态调用的调用点通过引导方法连接到一个方法上,这个引导方法是由动态语言编译器指定的,并被JVM调用一次来连接到位置上。假设编译器使用动态调用指令调用+,并且假设运行时系统知道

adder(Integer,Integer) 方法,那么运行时能够连接动态调用的调用点到 adder方法上,如下所示:

invoke函数 java_常量池

在这个例子中,IntegerOps类属于动态语言的运行时系统库。

Example.mybsm是一个连接动态调用点到adder方法的引导方法。

callerClass对象是一个查找对象,它是创建方法句柄的工厂。

callerClass对象调用MethodHandles.Lookup.findStatic方法创建了一个adder方法的静态方法句柄。

注意:这个引导方法仅连接了动态调用点到adder方法定义的代码上,并且它假设动态调用点给出的参数将是Integer对象。如果引导方法的参数不同(这个例子中是

callerClass, dynMethodName,

和dynMethodType),引导方法需要额外的代码来正确地连接调用点到合适的代码上执行。

java.lang.invoke.MethodHandles和java.lang.invoke.MethodHandle类包含了各种基于现有方法句柄来创建方法句柄的方法。这个例子中,如果方法句柄mh的方法类型不匹配参数dynMethodType的方法类型,则调用asType方法。这使得引导方法能够连接动态调用点到方法类型不完全匹配的Java方法上。

由引导方法返回的ConstantCallSite实例表示一个和特定动态调用指令关联的调用点。ConstantCallSite实例的目标是持久的、不能改变。在这种情况下,只有一个Java方法adder是执行调用点的候选方法。注意这个方法不一定是一个Java方法。相反如果有多个这样的方法对于运行时系统可用,每种处理不同的参数类型,引导方法

mybsm可以基于dynMethodType参数动态地选择正确的方法。

动态调用指令

动态调用指令简化并潜在改善了JVM上动态语言的编译器和运行时系统的实现。动态调用指令是通过允许语言实现者自定义连接行为做到这点的。这是和其它JVM指令对比而言的,如虚调用指令(invokevirtual)的连接行为是特定于Java类和接口的,是由JVM硬连接的。

每个动态调用指令(invokedynamic instruction)的实例被称为一个动态调用点(dynamic call

site)。一个动态调用点最初是未连接的状态,表示这个调用点没有指定可调用的方法。如前面所述,动态调用点通过引导方法连接到一个方法上。引导方法是由动态语言编译器所指定的方法,并被JVM调用一次来连接到位置上。引导方法返回的对象持久地决定了调用点的行为。

动态调用指令包含一个常量池索引(和其它invoke指令的格式一样)。常量池索引引用了一个CONSTANT_InvokeDynamic条目。这个条目指定了引导方法(一个CONSTANT_MethodHandle条目)、动态连接方法的名字和调用该动态连接方法的参数类型与返回值类型。

下面的是一个动态连接指令的例子。在这个例子中,运行时系统通过使用引导方法

Example.mybsm,把动态调用指令(+操作符)指定的调用点连接到IntegerOps.adder方法上。adder和mybsm是前文中定义的方法。

(为清晰起见,已加入换行符) :

invoke函数 java_常量池

注意:这部分的字节码例子使用的是ASM Java字节码处理和分析框架的语法。

使用动态调用指令调用动态链接方法包括以下步骤:

定义引导方法

指定常量池条目

使用动态连接指令

1.定义引导方法

在运行时,当JVM首次遇到动态调用指令时,它会调用引导方法。引导方法连接动态调用指令所指定的名字和应执行的代码(目标方法),目标方法由方法句柄所引用。如果JVM再次执行同样的动态调用指令,就不会再调用引导方法了,而是自动调用连接的方法句柄。

引导方法的返回类型必须是java.lang.invoke.CallSite。CallSite对象表示动态调用指令的已连接状态和要连接的方法句柄。

引导方法使用三个或更多参数:

一个MethodHandles.Lookup对象,是在动态调用指令上下文中用来创建方法句柄的工厂。

一个String对象,动态调用点中提及的方法名。

一个MethodType对象,动态调用点已解析的类型签名。

可选的一个或多个动态调用指令的额外静态参数。这些来自常量池的参数是用来帮助语言实现者安全、紧凑地编码额外的元数据,它们对引导方法有帮助。理论上,方法名和额外的参数是冗余的,因为每个调用点可以获得它自己唯一的引导方法。然而,这样的实践很可能产生较大的类文件和常量池。

引导方法的示例请查看前面部分。

2.指定常量池条目

正如前面所述,动态调用指令包含了一个到常量池条目的引用,使用CONSTANT_InvokeDynamic标签。这个条目又包含了到常量池中其它条目的引用和到属性的引用。这节简要地描述动态调用指令所使用的常量池条目。更多信息请参考java.lang.invoke包的文档和Java虚拟机规范。

常量池示例

下面这个是Example类的常量池摘录,它包含了连接了方法+和adder方法的引导方法Example.mybsm:

invoke函数 java_常量池

在这个例子中动态调用指令的常量池条目包含了三个值:

CONSTANT_InvokeDynamic 标签

unsigned short数值0

常量池索引#234

数值0指到存储在BootstrapMethods属性中说明符数组的第一个说明符。引导方法说明符不是在常量池表中的;他们是包含在单独的说明符数组中的。每个引导方法说明符包含了一个CONSTANT_MethodHandle常量池条目的索引,这是引导方法自身。

下面是来自同一个常量池中说明BootstrapMethods属性的摘录,包含了这个引导方法说明符的数组:

invoke函数 java_常量池

引导方法mybsm的常量池条目方法句柄包含了三个值:

CONSTANT_MethodHandle 标签

Unsigned byte 值6

常量池索引 #232

数值6是REF_invokeStatic的子标签。这个子标签的更多信息在下节说明。

3.使用动态连接指令

下面的字节码使用动态调用指令,引导方法mybsm把动态调用点+连接到adder方法上。这个例子使用+方法完成数字40与2相加(为清晰起见,已加入换行符):

invoke函数 java_常量池

前4条指令把整数40和2放到堆栈上并且把它们装箱为

java.lang.Integer包装类型。第5个指令调用一个动态方法。这个指令用一个CONSTANT_InvokeDynamic标签引用到常量池条目:

invoke函数 java_常量池

这个条目中有4个字节紧跟CONSTANT_InvokeDynamic标签:

前两个字节引用到一个CONSTANT_MethodHandle条目,它引用了一个引导方法说明符:

invoke函数 java_常量池