为了更好的支持动态类型语言,Java7通过JSR292给JVM增加了一条新的字节码指令:invokedynamic。之后,JVM上面的一些动态类型语言,比如Groovy(2.0+)和JRuby(1.7.0+)都开始支持invokedynamic。不过让人意外的是,为动态语言量身定制的invokedynamic指令,居然也被用到了Java8的Lambda表达式(JSR335)实现上。本文会对invokedynamic(以下简写做indy)指令做出详细解释。
测试代码
Java7以及更早版本的Java语法是没有办法编译生成indy指令的,所以我用Java8(主要是利用它的Lambda表达式)写了一个测试类,如下:
public class InDyTest { public static void main(String[] args) { Runnable x = () -> { //System.out.println("Hello, World!"); }; } }
InvokeDynamic指令
根据JVM规范的规定,indy的操作码是186(0xBA),格式是:
invokedynamic indexbyte1 indexbyte2 0 0
InDy指令有四个操作数,前两个操作数构成一个索引[ (indexbyte1 << 8) | indexbyte2 ],指向类的常量池,后两个操作数保留,必须是0。索引指向的常量池项的类型为CONSTANT_InvokeDynamic_info。编译InDyTest.java,然后用javap
-v -p指令反编译生成的.class文件,找到main()方法,可以看到,确实生成了一条indy指令:
常量池索引为#2,查看常量池可知,确实是一个CONSTANT_InvokeDynamic_info:
-
CONSTANT_InvokeDynamic_info
CONSTANT_InvokeDynamic_info结构是Java7新引入class文件的,其用途就是给indy指令指定启动方法(bootstrap method)等信息。它本身又包含两个索引,如下所示:
CONSTANT_InvokeDynamic_info { u1 tag; u2 bootstrap_method_attr_index; u2 name_and_type_index; }
其中name_and_type_index索引类常量池里的CONSTANT_NameAndType_info, 这个结构在Java7之前就有,所以不过多介绍。从上面的截图可以知道,它描述的是这样一个方法:
Runnable run() { ... }
bootstrap_method_attr_index索引bootstrap_methods表,bootstrap_methods位于class文件的attributes表里。
BootstrapMethods属性
JVM规范规定,如果类的常量池中存在CONSTANT_InvokeDynamic_info的话,那么attributes表中就必定有且仅有一个BootstrapMethods属性。BootstrapMethods属性是个变长的表,结构如下所示:
BootstrapMethods_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_bootstrap_methods; { u2 bootstrap_method_ref; u2 num_bootstrap_arguments; u2 bootstrap_arguments[num_bootstrap_arguments]; } bootstrap_methods[num_bootstrap_methods]; }
每一个BootstrapMethod都包含一个bootstrap_method_ref和n个bootstrap_arguments。bootstrap_method_ref是个常量池索引,指向一个CONSTANT_MethodHandle_info。而每一个bootstrap_argument也都是常量池索引,可以指向下面这些结构:
- CONSTANT_String_info
- CONSTANT_Class_info
- CONSTANT_Integer_info
- CONSTANT_Long_info
- CONSTANT_Float_info
- CONSTANT_Double_info
- CONSTANT_MethodHandle_info
- CONSTANT_MethodType_info
下面我们继续看javap的反编译结果:
确实存在一个BootstrapMethods表,这个表中只有一个BootstrapMethod,它的bootstrap_method_ref是常量池#23,有三个bootstrap_arguments,分别指向常量池#24,#25和#24:
CONSTANT_MethodHandle_info
CONSTANT_MethodHandle_info结构包含两项信息,其结构如下所示:
CONSTANT_MethodHandle_info { u1 tag; u1 reference_kind; u2 reference_index; }
reference_kind是一个1到9之间的整数,具体含义可以参考JVM规范。reference_index是常量池索引,但具体索引的是什么类型的常量,需要看reference_kind:
constant_pool entry | reference_kind |
---|---|
CONSTANT_Fieldref_info | 1 (REF_getField), 2 (REF_getStatic), 3 (REF_putField), or 4 (REF_putStatic) |
CONSTANT_Methodref_info | 5 (REF_invokeVirtual), 6 (REF_invokeStatic), 7 (REF_invokeSpecial), or 8 (REF_newInvokeSpecial) |
CONSTANT_InterfaceMethodref_info | 9 (REF_invokeInterface) |
通过观察常量池#23可以看到,它的reference_kind是6(REF_invokeStatic),reference_index是#29,正好是个CONSTANT_Methodref_info:
-
也就是说,引用的是java.lang.invoke.LambdaMetafactory类的静态方法metafactory()。
引用关系图
由于Java类文件格式的原因,要想真正理解上面说的内容,确实有点困难。我画了一张图来帮助我们理解:
JVM如何执行indy指令
前面从class文件的角度,分析了indy指令。下面让我们看看JVM是如何执行indy指令的。
- 每一个indy指令出现的地方,都叫做一个dynamic call site(动态调用点)
- 根据indy指令的操作数,可以找到一个call site specifier(调用点说明符),根据前面的分析,这个说明符其实就常量池里的CONSTANT_InvokeDynamic_info
- JVM解析(resolve)调用点说明符,得到下面三种信息:
- 一个MethodHandle,指向bootstrap method(启动方法)
- 方法名和方法描述,表示动态调用的方法
- 其他提供给启动方法的参数
- 接着JVM调用启动方法,并把上一步提到的信息通过参数传给启动方法
- 启动方法必须返回一个CallSite对象,并且,这个CallSite对象将永久和这个动态调用点关联
- 调用跟CallSite关联的MethodHandle指向的方法
下面是一张示意图,画出了关键点:
LambdaMetafactory.metafactory()
下面看一下LambdaMetafactory.metafactory()方法的源代码:
public static CallSite metafactory(MethodHandles.Lookup caller, String invokedName, // run MethodType invokedType, // ()Runnable MethodType samMethodType, // ()void MethodHandle implMethod, // ()void MethodType instantiatedMethodType) // ()void throws LambdaConversionException { ... }
通过加断点运行InDyTest,可以看到传递给metafactory()方法的参数,我在上面的代码里进行了注释。
总结
indy指令看似简单,但实际上非常复杂。希望本文能够帮助你理解indy指令。
Java8 Lambda表达式深入学习(2) -- InvokeDynamic指令详解