本节主要针对Java 7中最复杂的新特性之一。尽管这个特性十分强大,但它并不是给所有开发人员准备的,它只会出现在非常高级的用例中。目前来看,这个特性是为框架开发人员和非Java语言准备的。
也就是说如果你对平台底层如何运转不感兴趣,对新的字节码细节毫不关心,请跳到小结部分或直接进入下一章,没关系的。
如果你还在,很好。接下来我们可以向你介绍invokedynamic
的出现是多么不同寻常。Java 7引入了一个崭新的字节码,这在Java世界中可是从来没有过的大事件。这个字节码新秀就是invokedynamic
,一种新的调用指令,是用来做方法调用的。它可以用来告诉VM必须延迟确定要调用哪个方法。也就是说VM不用像往常一样在编译或连接时就敲定所有细节。
相反,需要什么方法在运行时决定。通过调用一个辅助方法来确定应该调用哪个方法。
javac不会产生invokedynamic
在Java 7中,Java语言还不能直接支持
invokedynamic
,没有哪个Java表达式会被javac
直接编译成invokedynamic
。人们希望Java 8会增加更多的语言结构(比如默认方法)来使用这些动态能力。
invokedynamic
是为非java语言准备的。添加它是为了让动态语言能够利用Java 7 VM,不过有些聪明的Java框架也找到了让invokedynamic
为它们服务的办法。
我们在本节中会给出invokedynamic
的工作细节,还会给出一个详细的例子——反编译一个利用新字节码的调用点。注意,要使用那些用到invokedynamic
的语言和框架不一定要完全搞清楚这些内容。
5.5.1 invokedynamic如何工作
为了支持invokedynamic
,Java 7又新增加了几条常量池定义。这些是在Java 6技术中无法提供的支持。
给invokedynamic
指令的索引必须指向类型为CONSTANT_InvokeDynamic
的常量。这个常量上是两个16位的索引(也就是4字节)。第一个索引指向方法表(用来确定要调用什么)。它们被称为引导方法(有时简写为BSM),并且必须是静态的,还要有确定的参数签名。第二个索引指向CONSTANT_NameAndType
。
从中可以看出CONSTANT_InvokeDynamic
和普通的CONSTANT_MethodRef
差不多,只是CONSTANT_MethodRef
指明在哪个类的常量池里找寻方法,而invokedynamic
调用则通过引导方法来寻找答案。
引导方法会返回一个CallSite
实例,用它来接收与调用点相关的信息,并连接动态调用。调用点中有一个MethodHandle
,调用点在这里起一个代理的作用,对它的所有调用实际上就是对MethodHandle
的调用。1
1 详情请参见CallSite
的Javadoc:http://cr.openjdk.java.net/~jrose/pres/indy-javadoc-mlvm/java/lang/invoke/CallSite.html。——译者注
invokedynamic
一开始并没有目标方法(还没连接)。在第一次调用时,该点的引导方法被调用。引导方法返回一个CallSite
,它被连接到invokedynamic
指令上。该过程如图5-5所示。
图5-5 虚拟vs动态调用
连接上CallSite
后,就可以调用真正的方法了,即CallSite
持有的MethodHandle
所指向的方法。这种设定表明JIT编译器可以像优化invokevirtual
调用那样优化invokedynamic
调用。下一章会讨论更多有关优化的内容。
还有一点值得注意,某些CallSite
对象是可以重连的(在它们的生命期内指向不同的目标方法)。一些动态语言会大量使用这一特性。
下一节会给出一个简单的例子,我们可以看到invokedynamic
调用在字节码中如何表示。
5.5.2 示例:反编译invokedynamic调用
如前所述,Java 7中没有支持invokedynamic
的Java语法。要得到带有动态调用指令的.class文件,你只能向字节码处理类库求助。ASM类库(http://asm.ow2.org/)就是一个不错的选择——它是一个工业级类库,在Java框架中得到了广泛应用。
我们可以用这个类库构造一个包含invokedynamic
指令的类,然后将其转换为字节流。这既可以写到磁盘里,也可以交给类加载器插入到运行的VM中。
一个简单的例子是让ASM产生的类包含一种invokedynamic
指令的静态方法。这个方法可以由普通的Java代码调用——它封装(或隐藏)了真正调用的动态本质。作为 invokedynamic
开发工作的一部分,Remi Forax和ASM团队提供了一个简单的工具来产生这样的测试类。ASM是第一批完全支持新字节码的工具之一。
让我们来看一下这种封装方法的字节码:
public static java.math.BigDecimal invokedynamic; Code: 0: invokedynamic #22, 0 // InvokeDynamic #0:_:Ljava/math/BigDecimal; 5: areturn
到目前为止还没什么看头,因为复杂性主要体现在常量池中。我们来看看和动态调用相关的常量池条目:
BootstrapMethods: 0: #17 invokestatic test/invdyn/DynamicIndyMakerMain.bsm: ➥ (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String; ➥ Ljava/lang/invoke/MethodType;Ljava/lang/Object;) ➥ Ljava/lang/invoke/CallSite; Method arguments: #19 1234567890.1234567890 #10 = Utf8 Ljava/math/BigDecimal; #18 = Utf8 1234567890.1234567890 #19 = String #18 // 1234567890.1234567890 #20 = Utf8 _ #21 = NameAndType #20:#10 // _:Ljava/math/BigDecimal; #22 = InvokeDynamic #0:#21 // #0:_:Ljava/math/BigDecimal;
要想完全搞清楚确实得花点心思琢磨琢磨。我们逐一来看一下。
invokedynamic
操作码在条目#22中。它指向引导方法#0和NameAndType
#21。- 在#0的BSM是类
DynamicIndyMakerMain
中的普通静态方法bsm
。它有BSM的正确签名。 - 条目#21给出了这个动态连接点的名称“_”,还有返回类型
BigDecimal
(保存在#10)。 - 条目#19是传入引导方法的静态参数。
如你所见,这里需要做很多基础工作来保证类型安全。但在运行时出错的方式仍然还有很多,但这种机制作了很大贡献,它在保留了灵活性的同时提供了安全性。
注意
BootstrapMethods
方法指向方法句柄而不是直接指向方法,这提供了额外的间接性,或者说灵活性。在前面的讨论中我们并没有涉及,因为它可能会混淆正在发生的事情,对于理解这种机制如何工作并没有实质性的帮助。
到此为止,我们已经结束了对invokedynamic
和字节码及类加载内部工作机制的讨论。