8.4.4 invokedynamic指令

8.4.4 invokedynamic指令

8.4节一开始就提到了JDK 7为了更好地支持动态类型语言,引入了第五条方法调用的字节码指令 invokedynamic,之后却一直没有再提起它,甚至把代码清单8-12使用MethodHandle的示例代码反编译 后也完全找不到invokedynamic的身影,这实在与invokedynamic作为Java诞生以来唯一一条新加入的字 节码指令的地位不相符,那么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对象,最终调用到要执行的目标方法上。我们还是照例不依赖枯燥的概念描述,改用一个实际例子来解释这个过程吧,如代码清单8-13所示。

代码清单8-13 InvokeDynamic指令演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class InvokeDynamicTest {
public static void main(String[] args) throws Throwable {
INDY_BootstrapMethod().invokeExact("icyfenix");
}
public static void testMethod(String s) {
System.out.println("hello String:" + s);
}
public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable {
return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt));
}
private static MethodType MT_BootstrapMethod() {
return MethodType .fromMethodDescriptorString( "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", null);
}
private static MethodHandle MH_BootstrapMethod() throws Throwable {

return lookup().findStatic(InvokeDynamicTest.class, "BootstrapMethod", MT_BootstrapMethod());
}
private static MethodHandle INDY_BootstrapMethod() throws Throwable {
CallSite cs = (CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(), "testMethod", MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V", null));
return cs.dynamicInvoker();
}
}

这段代码与前面MethodHandleTest的作用基本上是一样的,虽然笔者没有加以注释,但是阅读起来应当也不困难。要是真没读懂也不要紧,笔者没写注释的主要原因是这段代码并非写给人看的,只是为了方便编译器按照笔者的意愿来产生一段字节码而已。前文提到过,由于invokedynamic指令面向的主要服务对象并非Java语言,而是其他Java虚拟机之上的其他动态类型语言,因此,光靠Java语言的编译器Javac的话,在JDK 7时甚至还完全没有办法生成带有invokedynamic指令的字节码(曾经有一个java.dyn.InvokeDynamic的语法糖可以实现,但后来被取消了),而到JDK 8引入了Lambda表达式和接口默认方法后,Java语言才算享受到了一点invokedynamic指令的好处,但用Lambda来解释invokedynamic指令运作就比较别扭,也无法与前面MethodHandle的例子对应类比,所以笔者采用一些变通的办法:John Rose(JSR 292的负责人,以前Da Vinci Machine Project的Leader)编写过一个把程序的字节码转换为使用invokedynamic的简单工具INDY^1来完成这件事,我们要使用这个工具来产生最终需要的字节码,因此代码清单8-13中的方法名称不能随意改动,更不能把几个方法合并到一起写, 因为它们是要被INDY工具读取的。

把上面的代码编译,再使用INDY转换后重新生成的字节码如代码清单8-14所示(结果使用javap输出,因版面原因,精简了许多无关的内容)。

代码清单8-14 InvokeDynamic指令演示(2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Constant pool:
#121 = NameAndType #33:#30 // testMethod:(Ljava/lang/String;)V
#123 = InvokeDynamic #0:#121 // #0:testMethod:(Ljava/lang/String;)V

public static void main(java.lang.String[]) throws java.lang.Throwable;
Code:
stack=2, locals=1, args_size=1
0: ldc #23 // String abc
2: invokedynamic #123, 0 // InvokeDynamic #0:testMethod: (Ljava/lang/String;)V
7: nop
8: return

public static java.lang.invoke.CallSite BootstrapMethod(java.lang.invoke.Method Handles$Lookup, java.lang.String, java.lang.invoke.MethodType) throws java.lang.Throwable;
Code:
stack=6, locals=3, args_size=3
0: new #63 // class java/lang/invoke/ConstantCallSite
3: dup
4: aload_0
5: ldc #1 // class org/fenixsoft/InvokeDynamicTest
7: aload_1
8: aload_2
9: invokevirtual #65 // Method java/lang/invoke/MethodHandles$ Lookup.findStatic:(Ljava/lang/Class;Ljava/ lang/String;Ljava/lang/invoke/Method Type;)Ljava/lang/invoke/MethodHandle;
12: invokespecial #71 // Method java/lang/invoke/ConstantCallSite. "<init>":(Ljava/lang/invoke/MethodHandle;)V
15: areturn

从main()方法的字节码中可见,原本的方法调用指令已经被替换为invokedynamic了,它的参数为第123项常量(第二个值为0的参数在虚拟机中不会直接用到,这与invokeinterface指令那个的值为0的参数一样是占位用的,目的都是为了给常量池缓存留出足够的空间):

1
2: invokedynamic #123, 0 // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V

从常量池中可见,第123项常量显示“#123=InvokeDynamic#0:#121”说明它是一项CONSTANT_InvokeDynamic_info类型常量,常量值中前面“#0”代表引导方法取Bootstrap Methods属性表的第0项(javap没有列出属性表的具体内容,不过示例中仅有一个引导方法,即BootstrapMethod()),而后面的“#121”代表引用第121项类型为CONSTANT_NameAndType_info的常量,从这个常量中可以获取到方法名称和描述符,即后面输出的“testMethod: (Ljava/lang/String;)V”。

再看BootstrapMethod(),这个方法在Java源码中并不存在,是由INDY产生的,但是它的字节码很容易读懂,所有逻辑都是调用MethodHandles$Lookup的findStatic()方法,产生testMethod()方法的MethodHandle,然后用它创建一个ConstantCallSite对象。最后,这个对象返回给invokedynamic指令实现对testMethod()方法的调用,invokedynamic指令的调用过程到此就宣告完成了。