在JDK7中,Java提供了对动态语言特性的支持,实现了JSR 292 《Supporting Dynamically Typed Languages on the Java Platform》规范,这是Java语言发展的一重大进步,而提供对动态语言特性支持也是Java发展的一大趋势与方向。那么动态性表现在哪里呢?其一在Java API层面,新增了java.lang.invoke包,主要包含了CallSite、MethodHandle、MethodType等类;其二,在Java字节码指令层面,新增了invokedynamic指令,而伴随invokedynamic指令新增而在Class类文件常量池中新增了CONSTANT_InvokeDynamic_info,
CONSTANT_MethodHandle_info, CONSTANT_MethodType_info常量表、新增BootstrapMethods属性表。
那么什么是动态性,与动态相对的即是静态。大家应该都听说过,Java是一门静态型语言(C++也是),而动态型语言有Groovy、JavaScript、Ruby、Phthon、PHP、Lisp等等。从这个列举中可以发现,动态语言一大堆,而静态语言最常见的就是Java和C++了,这也从侧映证了动态性是语言发展的趋势。这些动态型语言在语言语法层面上最大的特点就是变量用var/def声明;而是Java中,声明变量时必须指定该变量的类型,如:String name = "zhangsan"。 从深层次一点来讲,动态型语言声明的变量在编译期无法确定该变量的具体类型,只有到了运行时才能确定该变量的具体类型,而静态型语言,变量的具体类型(这里指变量静态类型,多态性不包含在此)在编译期就已经确定,以Java为例:声明了一个实例变量,经过编译器编译后,该变量的简单名称和描述符符号引用都已经存储在了Class文件字节码中,在类的解析阶段,虚拟机就会将变量的符号引用解析为直接引用。这个直接引用将会解析为什么类型,在编译期就已经确定了。下面举个具体的例子:
动态语言以JavaScript为例
function Output() { } Output.prototype.println = function(_value) { console.info(_value);//FireFox中 } //执行语句 var output = new Output(); output.println();
在上述代码中,Output类型对象有一个println方法,而在实际运行中,变量output却不一定非得是Output类型。只要output变量指向的对象(方法的接收者)上有println方法就可以,而不管接收者到底是什么类型。
而在Java中,以最有名的HelloWorld为例:
public static void main(String[] args) { System.out.println("HelloWorld"); }
System.out被声明为java.io.PrintStream类型,所以System.out对象就必须是java.io.PrintStream类型或者java.io.PrintStream的子类。这里你也许会话,这不对旬真正类型也是可以改变的嘛。的确,但是这个“改变”却有很大的限制,对象真实类型必须是声明类型或者声明类型的子类型,这个是语言多态性最基础的保证。而JDK7对动态性的支持希望做到的是类型JavaScript中一样,对象的真实类型由运行期确定。
下面就举一个使用java.lang.invoke包完成动态方法调用的例子:
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.util.Random; public class Output { public void println(Object value) { System.out.println("value=" + value); } public static MethodHandle getMethodHandle(Object receiver) throws Throwable { //如果Lookup对象 MethodHandles.Lookup lookup = MethodHandles.lookup(); //MethodType代表方法的类型(不包含方法名称),其实MethodType是为了确定方法的描述符,例如此方法描述符为:(Ljava/lang/Object;)V MethodType methodType = MethodType.methodType(void.class, Object.class); //在接收者类中查找一个名为println,指定方法类型的虚方法 return lookup.findVirtual(receiver.getClass(), "println", methodType).bindTo(receiver); } public static void main(String[] args) throws Throwable { Object receiver = new Random().nextInt(1000)%2==0 ? System.out : new Output(); // 无论receiver最终是什么类型,只要有println方法,方法就可以正常调用。 getMethodHandle(receiver).invoke("Hello Dynamic Invoke"); } }
你会发现这时候,无论receiver是什么类型的对象,只要其有一个名为println带一个参数的方法,程序就可以运行,这样就可以实现方法接收者的动态确定。
从上面的例子看来,Java动态性使用很简单,不过看完它的用法之后,大家也许会有疑问,相同的事情,用反射不是早就可以实现了吗?确实,仅站在Java语言的角度看,MethodHandle的使用方法和效果上与Reflection都有众多相似之处。不过,它们也有以下这些区别:
1. Reflection和MethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.Lookup上的三个方法findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual
& invokeinterface和invokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用Reflection API时是不需要关心的。
2. Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息来得多。前者是方法在Java一端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含有执行权限等的运行期信息。而后者仅仅包含着与执行该方法相关的信息。用开发人员通俗的话来讲,Reflection是重量级,而MethodHandle是轻量级。
3. 由于MethodHandle是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),在MethodHandle上也应当可以采用类似思路去支持(但目前实现还不完善)。而通过反射去调用方法则不行。
MethodHandle与Reflection除了上面列举的区别外,最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度看”之后:Reflection API的设计目标是只为Java语言服务的,而MethodHandle则设计为可服务于所有Java虚拟机之上的语言,其中也包括了Java语言而已。
在JDK7以前,用于方法调用的字节码指令只有invokeinterface、invokestatic、invokespecial、invokevitual四个,这四个指令的具体含义与行为可以参看具体资料,最权威的当然是Java虚拟机规范。这四个指令方法调用时概括有4个要素:
1.方法名称:要调用的方法的名称一般是由开发人员在源代码中指定的符号名称。这个名称同样会出现在编译之后的字节代码中。
2.链接:链接包含了要调用方法的类。这一步有可能会涉及类的加载。
3.选择:选择要调用的方法。在类中根据方法名称和参数选择要调用的方法。
4.适配:调用者和接收者对调用的方式达成一致,即对方法的类型声明达成共识。
确定了上面4个要素之后,Java虚拟机会把控制权转移到被调用的方法中,并把调用时的实际参数传递过去。
这4个指令在动态性方面颇具有短板,如方法名称与描述符在字节码中就已经确定下来无法改变,4个指令都带有一个运行时常量池索引的参数,指向一个CONSTANT_Methodref_info表,CONSTANT_Methodref_info表包含了该方法所在类的信息(CONSTANT_Class_info索引),这样,方法所在类也不能动态变化,也就决定了方法的接收者不可动态改变(多态性不包含在此)。
在JDK7中,添加了invokedynamic指令,指令格式如下:
indexbyte1与indexbyte2组成一个运行时常量池索引,索引处为一个 CONSTANT_InvokeDynamic表,也被称为调用点描述符(call site specifier),第3与第4个操作数必须为0。CONSTANT_InvokeDynamic表中包含了启动方法(bootstrap method)、动态连接方法名称返回值参数列表等信息。需要注意的一点是CONSTANT_InvokeDynamic表bootstrap_method_attr_index数据项不是运行时常量池的索引,而是BootstrapMethods属性中包含的启动方法数组索引,具体可以查阅Java虚拟机规范中的invokedynamic指令的详细介绍,还有这一篇文章。
invokedynamic指令放宽了方法调用的限制,提升了方法调用的灵活性,以上面方法调用的四个要素来说明:
1.在方法的名称方面,不一定是符合Java命名规范的字符串,可以任意指定。方法的调用者和提供者也不需要在方法名称上达成一致。
2.提供了更加灵活的链接方式。一个方法调用所实际调用的方法可以在运行时再确定。这就相当于把链接操作推迟到了运行时,而不是必须在编译时就确定下来。对于一个已经链接好的方法调用,也可以重新进行链接,让它指向另外的方法。
3.在方法选择方面,不再是只能在方法调用的接收者上进行发派,而是可以考虑所有调用时的参数,即支持方法的多派发。
4.在调用之前,可以对参数进行各种不同的处理,包括类型转换、添加和删除参数、收集和分发可变长度参数等。
如果将上面动态方法调用的例子执行javap命令后,得到如下结果(getMethodHandle与main方法):
public static java.lang.invoke.MethodHandle getMethodHandle(java.lang.Object) throws java.lang.Throwable; flags: ACC_PUBLIC, ACC_STATIC Exceptions: throws java.lang.Throwable Code: stack=4, locals=3, args_size=1 0: invokestatic #48 // Method java/lang/invoke/MethodHandles.lookup:()Ljava/lang/invoke/MethodHandles$Lookup; 3: astore_1 4: getstatic #54 // Field java/lang/Void.TYPE:Ljava/lang/Class; 7: ldc #3 // class java/lang/Object 9: invokestatic #60 // Method java/lang/invoke/MethodType.methodType:(Ljava/lang/Class;Ljava/lang/Class;)Ljava/lang/invoke/MethodType; 12: astore_2 13: aload_1 14: aload_0 15: invokevirtual #66 // Method java/lang/Object.getClass:()Ljava/lang/Class; 18: ldc #70 // String println 20: aload_2 21: invokevirtual #71 // Method java/lang/invoke/MethodHandles$Lookup.findVirtual:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle; 24: aload_0 25: invokevirtual #77 // Method java/lang/invoke/MethodHandle.bindTo:(Ljava/lang/Object;)Ljava/lang/invoke/MethodHandle; 28: areturn LineNumberTable: line 16: 0 line 18: 4 line 20: 13 LocalVariableTable: Start Length Slot Name Signature 0 29 0 receiver Ljava/lang/Object; 4 25 1 lookup Ljava/lang/invoke/MethodHandles$Lookup; 13 16 2 methodType Ljava/lang/invoke/MethodType; public static void main(java.lang.String[]) throws java.lang.Throwable; flags: ACC_PUBLIC, ACC_STATIC Exceptions: throws java.lang.Throwable Code: stack=2, locals=2, args_size=1 0: new #87 // class java/util/Random 3: dup 4: invokespecial #89 // Method java/util/Random."<init>":()V 7: sipush 1000 10: invokevirtual #90 // Method java/util/Random.nextInt:(I)I 13: iconst_2 14: irem 15: ifne 24 18: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream; 21: goto 31 24: new #1 // class com/xtayfjpk/asm/test/dynamicinvoke/demo/Output 27: dup 28: invokespecial #94 // Method "<init>":()V 31: astore_1 32: aload_1 33: invokestatic #95 // Method getMethodHandle:(Ljava/lang/Object;)Ljava/lang/invoke/MethodHandle; 36: ldc #97 // String Hello Dynamic Invoke 38: invokevirtual #99 // Method java/lang/invoke/MethodHandle.invoke:(Ljava/lang/String;)V 41: return LineNumberTable: line 26: 0 line 28: 32 line 29: 41 LocalVariableTable: Start Length Slot Name Signature 0 42 0 args [Ljava/lang/String; 32 10 1 receiver Ljava/lang/Object; StackMapTable: number_of_entries = 2 frame_type = 24 /* same */ frame_type = 70 /* same_locals_1_stack_item */ stack = [ class java/lang/Object ]
你会发现,在Code属性中,完全找不到invokedynamic指令的影子,这是因为invokedynamic指令是提供给动态编译器使用的,而我们编译时用的是javac编译器,javac它不会生成invokedynamic指令。作为程序员,更多的还是使用java.lang.invoke包中的类来完全方法的动态调用,如果你实现了某种动态编译器在Code属性中生成了invokedynamic指令,虚拟机照样是可以正常执行的。
在字节代码中每个出现的invokedynamic指令都成为一个动态调用点(dynamic call site)。每个动态调用点在初始化的时候,都处于未链接的状态。在这个时候,这个动态调用点并没有被指定要调用的实际方法。当虚拟机要执行dynamic指令时,首先要链接到动态调用点,而动态调用点是由一个被称为启动方法(bootstrap)的方法确定的,启动方法的返回值就是CallSite。CallSite上会绑定一个MethodHandle,称为CallSite的目标,通过MethodHandle方法句柄就可以定位到真正在执行的方法。也就是说,对invokedynamic指令的调用实际上就等价于对方法句柄的调用,具体来说是被转换成对方法句柄的invoke方法的调用。
在JDk7中一共提供了三种CallSite,分别是ConstantCallSite,MutableCallSite与VolatileCallSite,这三个类都是CallSite的子类,ConstantCallSite的特点是其目标绑定是永久的,一但绑定就不能再进行更改,也就是一条invokedynamic指令链接上了一个ConstantCallSite后,其MethodHandle方法句柄不能再改变。MutableCallSite与ConstantCallSite是相对的,其目标绑定后可以进行更改。VolatileCallSite的目标有类似volatile变量的特点,当invokedynamic链接到一个VolatileCallSite调用点时,调用点的目标的更改invokedynamic指令立即就可以观察得到,即使这个更改是在其它线程中完成的。
下面我们就手动生成invokedynamic指令,看看虚拟机是否可以正常执行:
由于javac编译生成的字节码中不包含invokedynamic指令,所以我们无法看到。尽管如此,我们可以使用字节码生成工具(如ASM)来手动生成。
import static org.objectweb.asm.Opcodes.*; import java.lang.invoke.CallSite; import java.lang.invoke.ConstantCallSite; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; import java.lang.invoke.MethodHandles.Lookup; import java.nio.file.Files; import java.nio.file.Paths; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Handle; import org.objectweb.asm.MethodVisitor; public class DynamicInvokeInstructionGenerator { //启动方法定义 public static CallSite bootstrap(Lookup lookup, String name, MethodType type, String value) throws Exception { MethodHandle handle = lookup.findVirtual(StringBuilder.class, name, MethodType.methodType(StringBuilder.class)).bindTo(new StringBuilder(value)); return new ConstantCallSite(handle); } //ASM中定义的方法句柄 private static final Handle BSM = new Handle( H_INVOKESTATIC, DynamicInvokeInstructionGenerator.class.getName().replace('.', '/'), "bootstrap", MethodType.methodType(CallSite.class, Lookup.class, String.class, MethodType.class, String.class).toMethodDescriptorString()); public static void main(String[] args) throws Exception { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); cw.visit(V1_7, ACC_PUBLIC|ACC_SUPER, "StringReverser", null, "java/lang/Object", null); //生成main方法 MethodVisitor mv = cw.visitMethod(ACC_PUBLIC|ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); //调用StringBuilder的reverse方法 mv.visitInvokeDynamicInsn("reverse", "()Ljava/lang/StringBuilder;", BSM, "Hello Dynamic Invoke");//生成invokedynamic指令 //调用System.out.println(Object x) mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V"); mv.visitInsn(RETURN); mv.visitMaxs(0, 0); mv.visitEnd(); cw.visitEnd(); Files.write(Paths.get("StringReverser.class"), cw.toByteArray()); } }
只是为了书写方便,就把启动方法,定义方法句柄,生成字节码的代码写在一个类中了,对生成的StringReverser类执行javap命令后得到如果结果:
{ public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream; 3: invokedynamic #25, 0 // InvokeDynamic #0:reverse:()Ljava/lang/StringBuilder; 8: invokevirtual #31 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 11: return }
可以看到,invokedynamic指令的确生成了,执行该类得到输出:ekovnI cimanyD olleH,正确地执行了字符串反序操作。
至此手动生成字节码成功并顺序执行。
关于启动方法签名,java.lang.invoke包中类的更详细信息可参看:http://docs.oracle.com/javase/7/docs/api/java/lang/invoke/package-summary.html