转载

JVM如何处理方法调用

所有方法在Class文件都是一个常量池中的符号引用,类加载的解析阶段会将其转换成直接引用,这种解析的前提是:要保证这个方法在运行期是不可变的。这类方法的调用称为解析。

jvm提供了5条方法调用字节码指令:

  • [ ] invokestatic:调用静态方法
  • [ ] invokespecial:调用构造器方法、私有方法和父类方法
  • [ ] invokevirtual:调用所有的虚方法。
  • [ ] invokeinterface:调用接口方法,会在运行时期再确定一个实现此接口的对象
  • [ ] invokedynamic: 现在运行时期动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条指令,分派逻辑都是固化在虚拟机里面的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。InvokeDynamic指令详细请点击InvokeDynamic指令

invokestaticinvokespecial 指令调用的方法,都能保证方法的不可变性,符合这个条件的有 静态方法私有方法实力构造器父类方法 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");
        }
    }
}
复制代码
JVM如何处理方法调用

解析调用一定是一个静态的过程,在编译期间就可以完全确定,在类装载的解析阶段就会把涉及的符号引用全部转化为可确定的直接引用,不会延迟到运行期去完成。而分派调用可能是静态的也可能是动态的,根据分派一句的宗量数可分为单分派和多分派。因此分派可分为:静态单分派、静态多分派、动态单分派、动态多分派。

静态分派(方法重载)

所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。

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 称为变量的静态类型或者叫外观类型,吧 MiIphone 称为实际类型,静态类型仅仅在使用时发生变化,编译可知;实际类型在运行期才知道结果,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

所以,jvm重载时是通过参数的静态类型而不是实际类型作为判定依据。下图可以证明:

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 指令吧常量池中的类方法符号引用解析到了不同的直接引用上。

JVM如何处理方法调用

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()方法。

仅站在Java语言的角度看,MethodHandle的使用方法和效果上与Reflection都有众多相似之处。不过,它们也有以下这些区别:

  • ReflectionMethodHandle 机制本质上都是在模拟方法调用,但是 Reflection 是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在 MethodHandles.Lookup 上的三个方法 findStatic()findVirtual()findSpecial() 正是为了对应于 invokestaticinvokevirtual & invokeinterfaceinvokespecial 这几条字节码指令的执行权限校验行为,而这些底层细节在使用 Reflection API 时是不需要关心的。
  • Reflection 中的 java.lang.reflect.Method 对象远比 MethodHandle 机制中的 java.lang.invoke.MethodHandle 对象所包含的信息来得多。前者是方法在Java一端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含有执行权限等的运行期信息。而后者仅仅包含着与执行该方法相关的信息。用开发人员通俗的话来讲, Reflection重量级 ,而 MethodHandle轻量级
  • 由于 MethodHandle 是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),在 MethodHandle 上也应当可以采用类似思路去支持(但目前实现还不完善)。而通过反射去调用方法则不行。
  • MethodHandleReflection 除了上面列举的区别外,最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度看”之后: Reflection API 的设计目标是只为Java语言服务的,而 MethodHandle 则设计为可服务于所有Java虚拟机之上的语言,其中也包括了Java语言而已。

invokedynamic指令

参考原文: 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虚拟机第二版》

原文  https://juejin.im/post/5b7265a5f265da27f8036ba8
正文到此结束
Loading...