丹丹学妹哭着对我说:学长,JVM 如何实现动态类型语言支持的呢
sasdasdsadsadsadsadasdasdasdasdsadsadsaasddad 异常代码示例
public static void main(String[] args) {
int[][][] array = new int[1][0][-1];
}
sdad 分析:
sdad①、Java 中是能正常编译的,但运行会出现NegativeArraySizeException
异常,《Java 虚拟机规范》中规定是一个运行时异常。
sdad②、C 中直接在编译期报错。
sdad 总结:一门语言的哪一种检查行为要在运行期进行,哪一种检查要在编译期进行并没有什么必然的因果逻辑关系。
[](
)Java 与动态类型
===========================================================================
背景介绍:
sdad 目前已经有许多动态类型语言运行于 Java 虚拟机之上了,如 Clojure、Groovy、Jython 和 JRuby 等,也就是说我们可以在 JVM 上实现静态类语言的严谨与动态类型语言的灵活。
sd ad 但是 JDK7 以前,JVM 层面对动态类型语言的支持一直都还有所欠缺,主要表现在方法调用方面,只有 4 条方法调用指令(invokevirtual、invokespecial、invokestatic、invokeinterface),且它们的第一个参数(操作数)都是被调用的方法的符号引用(CONSTANT_Methodref_info 或者 CONSTANT_InterfaceMethodref_info 常量)。
sda
sdad 这时就有一个问题:方法的符号引用在编译时产生,而动态类型语言只有在运行期才能确定方法的接收者。
sda
sdad 那怎么解决这个问题的呢?
sda
sdad ①、曲线救国:比如编译时留个占位符类型,运行时动态生成字节码实现具体类型到占位符类型的适配。但是这样有两个缺点:
sdaadⅠ、让动态类型语言实现的复杂度增加,也会带来额外的性能和内存开销 sdadⅡ、由于无法确定调用对象的静态类型,而导致的方法内联无法有效进行。
sda
sdaad 基于以上背景,在 JVM 层面上提供动态类型的直接支持就成为 Java 平台发展必须解决的问题,直到 JDK 7,JSR-292 提出了invokedynamic
指令以及java.lang.invoke
包解决了此问题。
[](
)java.lang.invoke 包
sdad 这个包的主要目的:在之前单纯依靠符号引用来确定调用的目标方法这条路之外,提供一种新的动态确定目标方法的机制,称为“方法句柄”(Method Handle)。
sda
sdad**[注]:** 在 Java 语言中做不到像 C 那样,单独把一个函数作为参数(函数指针的功劳)进行传递。普遍的做法是设计一个带有 compare()方法的 Comparator 接口,以实现这个接口的对象作为参数。比如void sort(List list, Comparator c)
。
sda
sdad 不过在有了方法句柄之后,Java 语言就可以像 C 那样,拥有类似于函数指针或者委托的方法别名这样的工具了。
sasdasdsad
sasdasdsadsadsadsadasdasdasdasdsadsadsaasddad 方法句柄演示
import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
public class MethodHandleTest {
static class ClassA {
public void println(String s) {
System.out.println(s);
}
}
public static void main(String[] args) throws Throwable {
Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
// 无论 obj 最终是哪个实现类,下面这句都能正确调用到 println 方法。
getPrintlnMH(obj).invokeExact("icyfenix");
}
private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
// MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)
//和具体参数(methodType()第二个及以后的参数)。
MethodType mt = MethodType.methodType(void.class, String.class); //pritln 的返回值、以及具体参数
/*lookup()方法来自于 MethodHandles.lookup,这句的作用是在指定类中查找(符合给定的方法名称、
方法类型,并且符合调用权限的)方法句柄。
因为这里调用的是一个虚方法,按照 Java 语言的规则,方法第一个参数是隐式的,代表该方法的接收者,
也即 this 指向的对象,这个参数以前是放在参数列表中进行传递,现在提供了 bindTo()方法来完成这件事情。
*/
return lookup().findVirtual(reveiver.getClass(), "println",mt).bindTo(reveiver);//方法类型、名称、即返回值和参数
}
}
方法解析:
sdad 方法 getPrintlnMH()中实际上是模拟了 invokevirtual 指令的执行过程,只不过它的分派逻辑并非固化在 Class 文件的字节码上,而是通过一个由用户设计的 Java 方法来实现。而这个方法本身的返回值(MethodHandle 对象),可以视为对最终调用方法的一个“引用”。
sdad 我们再次回顾 sort()方法,有了 MethodHandle 就可以写出类似于 C/C++那样的函数声明了:
sdasdasdasdasdsadasdsadasdasdasdasadvoid sort(List list, MethodHandle compare)
到此,我们发现 MethodHandle 在使用方法和效果上与 Reflection 有众多相似之处。但是它们也有 很大的区别:
sasad①、Reflection 和 MethodHandle 机制本质上都是在模拟方法调用。但是 Reflection 是在模拟 Java 代码层次的方法调用,而 MethodHandle 是在模拟字节码层次的方法调用。在 MethodHandles.Lookup 上的 3 个方法 findStatic()、findVirtual()、findSpecial()正是为了对应于 invokestatic、invokevirtual(以及 invokeinterface)和 invokespecial 这几条字节码指令的执行权限校验行为,而这些底层细节在使用 Reflection API 时是不需要关心的。
sasad②、Reflection 中的 java.lang.reflect.Method 对象远比 MethodHandle 机制中的 java.lang.invoke.MethodHandle 对象所包含的信息来得多。
ssadasdasad 前者是方法在 Java 端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的 Java 端表示方式,还包含执行权限等的运行期信息。
ssadasdasad 后者仅包含执行该方法的相关信息。
sasad③、Reflection API 的设计目标是只为 Java 语言服务的,而 MethodHandle 则设计为可服务于所有 Java 虚拟机之上的语言。
sasad 总之,Reflection 是重量级,而 MethodHandle 是轻量级。
sasad[注]:由于 MethodHandle 是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),在 MethodHandle 上也应当可以采用类似思路去支持(但目前实现还在继续完善中),而通过反射去调用方法则几乎不可能直接去实施各类调用点优化措施。
[](
)invokedynamic 指令
sdasdasdasda
invokedynamic 指令与 MethodHandle 方法的对比:
sdasdasdasda
sdaa 相同点:某种意义上可以说 invokedynamic 指令与 MethodHandle 机制的作用是一样的,都是为了解决原有 4 条“invoke*”指令方法分派规则完全固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户(广义的用户,包含其他程序语言的设计者)有更高的自由度。
sdasdasdasda
sdaa 不同点:invokedynamic 指令用字节码和 Class 中其他属性、常量来完成。MethodHandle 是用上层代码和 API 来实现。
sdasdasdasda
sdaa**[注]:** 每一处含有 invokedynamic 指令的位置都被称作"动态调用点"。这条指令的第一个参数(操作数)不再是代表方法符号引用的 CONSTANT_Methodref_info 常量,而是变为 JDK 7 时新加入的 CONSTANT_InvokeDynamic_info 常量,从这个新常量中可以得到 3 项信息:
sdsdasaa①、引导方法(Bootstrap Method,该方法存放在新增的 BootstrapMethods 属性中)②、方法类型 ③、名称
sdaa 引导方法是有固定的参数,并且返回值规定是 java.lang.invoke.CallSite 对象,这个对象代表了真正要执行的目标方法调用。根据 CONSTANT_InvokeDynamic_info 常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个 CallSite 对象,最终调用到要执行的目标方法上。
sasdasdsad
sasdasdsadsadsadsadasdasdasdassaasddad InvokeDynamic 指令演示
import static java.lang.invoke.MethodHandles.lookup;
评论