首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》5.5 Invokedynamic

关灯直达底部

本节主要针对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和字节码及类加载内部工作机制的讨论。