所有方法在Class文件都是一个常量池中的符号引用,类加载的解析阶段会将其转换成直接引用,这种解析的前提是:要保证这个方法在运行期是不可变的。这类方法的调用称为解析。
jvm提供了5条方法调用字节码指令:
被 invokestatic
和 invokespecial
指令调用的方法,都能保证方法的不可变性,符合这个条件的有 静态方法
、 私有方法
、 实力构造器
、 父类方法
4类。这些方法称为非虚方法。
public class Main { public static void main(String[] args) { //invokestatic调用 Test.hello(); //invokespecial调用 Test test = new Test(); } static class Test{ static void hello(){ System.out.println("hello"); } } } 复制代码
解析调用一定是一个静态的过程,在编译期间就可以完全确定,在类装载的解析阶段就会把涉及的符号引用全部转化为可确定的直接引用,不会延迟到运行期去完成。而分派调用可能是静态的也可能是动态的,根据分派一句的宗量数可分为单分派和多分派。因此分派可分为:静态单分派、静态多分派、动态单分派、动态多分派。
所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。
public class Test { static class Phone{} static class Mi extends Phone{} static class Iphone extends Phone{} public void show(Mi mi){ System.out.println("phone is mi"); } public void show(Iphone iphone){ System.out.println("phone is iphone"); } public void show(Phone phone){ System.out.println("phone parent class be called"); } public static void main(String[] args) { Phone mi = new Mi(); Phone iphone = new Iphone(); Test test = new Test(); test.show(mi); test.show(iphone); test.show((Mi)mi); } } 复制代码
执行结果:
phone parent class be called phone parent class be called phone is mi 复制代码
我们把上面代码中的 Phone
称为变量的静态类型或者叫外观类型,吧 Mi
和 Iphone
称为实际类型,静态类型仅仅在使用时发生变化,编译可知;实际类型在运行期才知道结果,编译器在编译程序的时候并不知道一个对象的实际类型是什么。
所以,jvm重载时是通过参数的静态类型而不是实际类型作为判定依据。下图可以证明:
根据上面的代码也可以看出,我们可以使用强制类型转换来使静态类型发生改变。
public class Test2 { static abstract class Phone{ abstract void show(); } static class Mi extends Phone{ @Override void show() { System.out.println("phone is mi"); } } static class Iphone extends Phone{ @Override void show() { System.out.println("phone is iphone"); } } public static void main(String[] args) { Phone mi = new Mi(); Phone iphone = new Iphone(); mi.show(); iphone.show(); mi = new Iphone(); mi.show(); } } 复制代码
phone is mi phone is iphone phone is iphone 复制代码
这个结果大家肯定都能猜到,但是你又没有想过编译器是怎么确定他们的实际变量类型的呢。这就关系到了 invokevirtual
指令,该指令的第一步就是在运行期确定接受者的实际类型。所以两次调用 invokevirtual
指令吧常量池中的类方法符号引用解析到了不同的直接引用上。
invokevirtual
指令的运行时解析过程大致分为以下几个步骤。
(1)找到操作数栈顶的第一个元素(对象引用)所指向的对象的实际类型,记作C; (2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回 java.lang.IllegalAccessError。
(3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证。 (4)如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError异常
。
动态语言的关键特征是它的类型检查的主体过程是在运行期间而不是编译期。相对的,在编译期间进行类型检查过程的语言(java、c++)就是静态类型语言。
运行时异常:代码只要不运行到这一行就不会报错。 连接时异常:类加载抛出异常。
它们都有自己的优点。静态类型语言在编译期确定类型,可以提供严谨的类型检查,有很多问题编码的时候就能及时发现,利于开发稳定的大规模项目。动态类型语言在运行期确定类型,有很大的灵活性,代码更简洁清晰,开发效率高。
public class MethodHandleTest { static class ClassA { public void show(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最终是哪个实现类,下面这句都能正确调用到show方法。 getPrintlnMH(obj).invokeExact("fantj"); } private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable { // MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及以后的参数)。 MethodType mt = MethodType.methodType(void.class, String.class); // lookup()方法来自于MethodHandles.lookup,这句的作用是在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄。 // 因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表该方法的接收者,也即是this指向的对象,这个参数以前是放在参数列表中进行传递,现在提供了bindTo()方法来完成这件事情。 return lookup().findVirtual(reveiver.getClass(), "show", mt).bindTo(reveiver); } } 复制代码
fantj 复制代码
无论obj是何种类型(临时定义的ClassA抑或是实现PrintStream接口的实现类System.out),都可以正确调用到show()方法。
Reflection
和 MethodHandle
机制本质上都是在模拟方法调用,但是 Reflection
是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在 MethodHandles.Lookup
上的三个方法 findStatic()
、 findVirtual()
、 findSpecial()
正是为了对应于 invokestatic
、 invokevirtual & invokeinterface
和 invokespecial
这几条字节码指令的执行权限校验行为,而这些底层细节在使用 Reflection API
时是不需要关心的。 Reflection
中的 java.lang.reflect.Method
对象远比 MethodHandle
机制中的 java.lang.invoke.MethodHandle
对象所包含的信息来得多。前者是方法在Java一端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含有执行权限等的运行期信息。而后者仅仅包含着与执行该方法相关的信息。用开发人员通俗的话来讲, Reflection
是 重量级 ,而 MethodHandle
是 轻量级 。 MethodHandle
是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),在 MethodHandle
上也应当可以采用类似思路去支持(但目前实现还不完善)。而通过反射去调用方法则不行。 MethodHandle
与 Reflection
除了上面列举的区别外,最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度看”之后: Reflection API
的设计目标是只为Java语言服务的,而 MethodHandle
则设计为可服务于所有Java虚拟机之上的语言,其中也包括了Java语言而已。 参考原文: blog.csdn.net/a_dreaming_…
一开始就提到了JDK 7为了更好地支持动态类型语言,引入了第五条方法调用的字节码指令invokedynamic,但前面一直没有再提到它,甚至把之前使用MethodHandle的示例代码反编译后也不会看见invokedynamic的身影,它到底有什么应用呢?
某种程度上可以说 invokedynamic
指令与 MethodHandle
机制的作用是一样的,都是为了解决原有四条 invoke*
指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户(包含其他语言的设计者)有更高的自由度。而且,它们两者的思路也是可类比的,可以想象作为了达成同一个目的,一个用上层代码和API来实现,另一个是用字节码和Class中其他属性和常量来完成。因此,如果前面 MethodHandle
的例子看懂了,理解 invokedynamic
指令并不困难。 每一处含有 invokedynamic
指令的位置都被称作“动态调用点(Dynamic Call Site)”,这条指令的第一个参数不再是代表方法符号引用的 CONSTANT_Methodref_info
常量,而是变为JDK 7新加入的 CONSTANT_InvokeDynamic_info
常量,从这个新常量中可以得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的 BootstrapMethods
属性中)、方法类型(MethodType)和名称。引导方法是有固定的参数,并且返回值是 java.lang.invoke.CallSite
对象,这个代表真正要执行的目标方法调用。根据 CONSTANT_InvokeDynamic_info
常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法上。我们还是照例拿一个实际例子来解释这个过程吧。如下面代码清单所示:
public class InvokeDynamicTest { public static void main(String[] args) throws Throwable { INDY_BootstrapMethod().invokeExact("icyfenix"); } public static void testMethod(String s) { System.out.println("hello String:" + s); } public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable { return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt)); } private static MethodType MT_BootstrapMethod() { return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", null); } private static MethodHandle MH_BootstrapMethod() throws Throwable { return lookup().findStatic(InvokeDynamicTest.class, "BootstrapMethod", MT_BootstrapMethod()); } private static MethodHandle INDY_BootstrapMethod() throws Throwable { CallSite cs = (CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(), "testMethod", MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V", null)); return cs.dynamicInvoker(); } } 复制代码
hello String:icyfenix 复制代码
看 BootstrapMethod()
,它的字节码很容易读懂,所有逻辑就是调用 MethodHandles$Lookup的findStatic()
方法,产生 testMethod()
方法的 MethodHandle
,然后用它创建一个 ConstantCallSite
对象。最后,这个对象返回给 invokedynamic
指令实现对 testMethod()
方法的调用, invokedynamic
指令的调用过程到此就宣告完成了。
重点参考:《深入java虚拟机第二版》