Java 杂谈

JVM--方法调用

2018-08-16  本文已影响7人  FantJ

方法调用不是方法执行,方法调用是让jvm确定调用哪个方法,所以,程序运行时的它是最普遍、最频繁的操作。jvm需要在类加载期间甚至运行期间才能确定方法的直接引用。

解析

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

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

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");
        }
    }
}

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

静态分派(方法重载)

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

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重载时是通过参数的静态类型而不是实际类型作为判定依据。下图可以证明:



根据上面的代码也可以看出,我们可以使用强制类型转换来使静态类型发生改变。

动态分派(方法覆盖)

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

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

invokedynamic指令

参考原文:https://blog.csdn.net/a_dreaming_fish/article/details/50635651

一开始就提到了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指令的调用过程到此就宣告完成了。

上一篇下一篇

猜你喜欢

热点阅读