本文共 5731 字,大约阅读时间需要 19 分钟。
在Java 7的发布版中包含了多项新的特性,这些特性乍看上去Java开发人员对它们的使用非常有限,在我们之前的中,曾经对其进行过介绍。
\\但是,其中有项特性对于实现Java 8中“头版标题”类型的特性来说至关重要(如lambdas和默认方法)。在本文中,我们将会深入学习invokedynamic,并阐述它对于Java平台以及像JRuby和Nashorn这样的JVM语言来讲为何如此重要。
\\至少始于2007年,而第一次成功的动态调用发生在2008年8月26日。这比Oracle收购Sun还要早,按照大多数开发人员的标准,这个特性的研发已经持续了相当长的时间。
\\值得注意的是,从Java 1.0到现在,invokedynamic是第一个新加入的Java字节码,它与已有的字节码invokevirtual、invokestatic、invokeinterface和invokespecial组合在了一起。已有的这四个操作码实现了Java开发人员所熟知的所有形式的方法分派(dispatch):
\\有些开发人员可能会好奇平台为何需要这四种操作码,所以我们看一个简单的样例,这个样例会用到不同的调用操作码,以此来阐述它们之间的差异:
\\\public class InvokeExamples {\ public static void main(String[] args) {\ InvokeExamples sc = new InvokeExamples();\ sc.run();\ }\\ private void run() {\ List ls = new ArrayList();\ ls.add(\"Good Day\");\\ ArrayList als = new ArrayList();\ als.add(\"Dydh Da\");\ }\}\\
我们可以使用javap反汇编从而得到它所产生的字节码:
\\\javap -c InvokeExamples.class\\public class kathik.InvokeExamples {\ public kathik.InvokeExamples();\ Code:\ 0: aload_0\ 1: invokespecial #1 // Method java/lang/Object.\"\":()V\ 4: return\\ public static void main(java.lang.String[]);\ Code:\ 0: new #2 // class kathik/InvokeExamples\ 3: dup\ 4: invokespecial #3 // Method \"\":()V\ 7: astore_1\ 8: aload_1\ 9: invokespecial #4 // Method run:()V\ 12: return\\ private void run();\ Code:\ 0: new #5 // class java/util/ArrayList\ 3: dup\ 4: invokespecial #6 // Method java/util/ArrayList.\"\":()V\ 7: astore_1\ 8: aload_1\ 9: ldc #7 // String Good Day\ 11: invokeinterface #8, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z\ 16: pop\ 17: new #5 // class java/util/ArrayList\ 20: dup\ 21: invokespecial #6 // Method java/util/ArrayList.\"\":()V\ 24: astore_2\ 25: aload_2\ 26: ldc #9 // String Dydh Da\ 28: invokevirtual #10 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z\ 31: pop\ 32: return\}\\
在这个示例中,展现了四个调用操作码中的三个(剩下的一个也就是invokestatic,是一个非常简单的扩展)。作为开始,我们可以看一下如下的两个调用(在run方法的字节11和28):
\\ls.add(\"Good Day\")
和
\\als.add(\"Dydh Da\")
在Java源码中它们看起来非常相似,但它们实际上却代表两种不同的字节码。
\\对于javac来说,变量ls具有的静态类型是List\u0026lt;String\u0026gt;
,而List是一个接口。所以,在运行时方法表(通常称为“vtable”)中,add()方法的精确位置还没有在编译时确定。因此,源码编译器会生成一个invokeinterface指令,将实际的方法查找推迟到运行期,也就是当ls的实际vtable能够探查到并且add()方法的位置能够找到的时候。
与之相反,对als.add(\"Dydh Da\")
的调用是通过als来执行的,这里的静态类型是类类型(class type)——ArrayList\u0026lt;String\u0026gt;
。这意味着在vtable中,方法的位置在编译期是可知的。因此,javac会针对这个精确的vtable条目生成一个invokevirtual指令。不过,最终的方法选择依然是在运行期确定的,因为这里还有方法重写(overriding)的可能性,但是vtable slot在编译期就已经确定了。
除此之外,这个样例还展现了invokespecial的两个使用场景。这个操作码用于在运行时确定如何分派的场景之中,具体来讲,在这里没有方法重写的需求,另外这也不可能实现。样例中所阐述的场景是private methods和super calls,这些方法在编译期是可知的,并且无法进行重写。
\\细心的读者可能已经发现,对Java方法的所有调用都编译成了四个操作码中的某一个,那么问题就来了——invokedynamic是做什么的,它对于Java开发人员有什么用处呢?
\\这个特性的主要目标在于创建一个字节码,用于处理新型的方法分派——它的本质是允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断。这样的话,相对于Java平台之前所提供的编程风格,允许语言和框架的编写人员支持更加动态的编码风格。
\\它的目的在于由用户代码通过方法句柄API(method handles API)在运行时确定如何分派,同时避免反射带来的性能惩罚和安全问题。实际上,invokedynamic所宣称的目标就是一旦该特性足够成熟,它的速度要像常规的方法分派(invokevirtual)一样快。
\\当Java 7发布的时候,JVM就已经支持执行新的字节码了,但是不管提交什么样的Java代码,javac都不会产生包含invokedynamic的字节码。这项特性用来支持JRuby和其他运行在JVM上的动态语言。
\\在Java 8中,这发生了变化,在实现lambda表达式和默认方法时,底层会生成和使用invokedynamic,它同时还会作为Nashorn的首选分派机制。但是,对于Java应用的开发人员来说,依然没有直接的方式实现完全的动态方法处理(resolution)。也就是说,Java语言并没有提供关键字或库来创建通用的invokedynamic调用点(call site)。这意味着,尽管这种机制的功能非常强大,但它对于大多数的Java开发人员来说依然有些陌生。接下来,我们看一下如何在自己的代码中使用这项技术。
\\要让invokedynamic正常运行,一个核心的概念就是方法句柄(method handle)。它代表了一个可以从invokedynamic调用点进行调用的方法。这里的基本理念就是每个invokedynamic指令都会与一个特定的方法关联(也就是引导方法或BSM)。当解释器(interpreter)遇到invokedynamic指令的时候,BSM会被调用。它会返回一个对象(包含了一个方法句柄),这个对象表明了调用点要实际执行哪个方法。
\\在一定程度上,这与反射有些类似,但是反射有它的局限性,这些局限性使它不适合与invokedynamic协作使用。Java 7 API中加入了java.lang.invoke.MethodHandle(及其子类),通过它们来代表invokedynamic指向的方法。为了实现操作的正确性,MethodHandle会得到JVM的一些特殊处理。
\\理解方法句柄的一种方式就是将其视为以安全、现代的方式来实现反射的核心功能,在这个过程会尽可能地保证类型的安全。invokedynamic需要方法句柄,另外它们也可以单独使用。
\\一个Java方法可以视为由四个基本内容所构成:
\\这意味着如果要引用某个方法,我们需要有一种有效的方式来表示方法签名(而不是反射中强制使用的令人讨厌的Class\u0026lt;?\u0026gt;[] hack方式)。
\\接下来我们采用另外的方式,方法句柄首先需要的一个构建块就是表达方法签名的方式,以便于查找。在Java 7引入的Method Handles API中,这个角色是由java.lang.invoke.MethodType类来完成的,它使用一个不可变的实例来代表签名。要获取MethodType,我们可以使用methodType()工厂方法。这是一个参数可变(variadic)的方法,以class对象作为参数。
\\第一个参数所使用的class对象,对应着签名的返回类型;剩余参数中所使用的class对象,对应着签名中方法参数的类型。例如:
\\\//toString()的签名\MethodType mtToString = MethodType.methodType(String.class);\\// setter方法的签名\MethodType mtSetter = MethodType.methodType(void.class, Object.class);\\// Comparator中compare()方法的签名\MethodType mtStringComparator = MethodType.methodType(int.class, String.class, String.class);\\
现在我们就可以使用MethodType,再组合方法名称以及定义方法的类来查找方法句柄。要实现这一点,我们需要调用静态的MethodHandles.lookup()方法。这样的话,会给我们一个“查找上下文(lookup context)”,这个上下文基于当前正在执行的方法(也就是调用lookup()的方法)的访问权限。
\\查找上下文对象有一些以“find”开头的方法,例如,findVirtual()、findConstructor()、findStatic()等。这些方法将会返回实际的方法句柄,需要注意的是,只有在创建查找上下文的方法能够访问(调用)被请求方法的情况下,才会返回句柄。这与反射不同,我们没有办法绕过访问控制。换句话说,方法句柄中并没有与setAccessible()对应的方法。例如:
\\\public MethodHandle getToStringMH() {\ MethodHandle mh = null;\ MethodType mt = MethodType.methodType(String.class);\ MethodHandles.Lookup lk = MethodHandles.lookup();\\ try {\ mh = lk.findVirtual(getClass(), \"toString\
转载地址:http://iinca.baihongyu.com/