深入理解 Java 虚拟机(九)方法调用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011330638/article/details/82729073

方法调用

方法调用不等同于方法执行,方法调用阶段唯一任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。一切方法调用在 Class 文件里面存储的都只是符号引用,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。

解析

在类加载的解析阶段,会把其中一部分方法的符号引用转化为直接引用,解析成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的,这类方法的调用称为解析。

在 Java 中,符合编译期可知,运行期不可变的要求的方法主要包括静态方法和私有方法,因此它们都适合在类加载阶段进行解析。

Java 虚拟机提供了 5 条方法调用的字节码指令:
1) invokestatic:调用静态方法
2) invokespecial:调用 < init> 方法、私有方法和父类方法
3) invokevirtual:调用所有的虚方法
4) invokeinterface:调用接口方法,在运行时再确定一个实现此接口的对象
5) invokedynamic:运行时动态解析出调用点限定符所引用的方法,然后再执行

前 4 条指令的分派逻辑是固定在 Java 虚拟机内部的,而 invokedynamic 指令的分派逻辑是由用户所设置的引导方法决定的。只要能被 invokestatic、invokespecial 指令调用的方法,都可以在解析阶段确定唯一的调用版本,它们在类加载的时候就会把符号引用解析为该方法的直接引用,这些方法可以称为非需方法,其它方法可以称为虚方法。

此外还有一种方法,就是被 final 修饰的方法,虽然 final 方法是使用 invokevirtual 指令来调用的,但 Java 语言规范中明确说明了 final 方法是一种非虚方法。

分派

解析是一个静态的过程,而分配调用可能是静态的,也可能是动态的,根据分配的宗量(后面会说明)数可以分配为单分派和多分派,因此共有静态单分派、静态多分派、动态单分派、动态多分派 4 种分派情况。

静态分派

有如下代码:

public class StaticDispatch {

    static abstract class Human {
    }

    static class Man extends Human {
    }

    static class Woman extends Human {
    }

    public void sayHello(Human guy) {
        System.out.println("hello,guy!");
    }

    public void sayHello(Man guy) {
        System.out.println("hello,gentleman!");
    }

    public void sayHello(Woman guy) {
        System.out.println("hello,lady!");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);
        sr.sayHello(woman);
    }

}

输出结果:

hello,guy!
hello,guy!

虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据的,因此,在编译阶段,Javac 编译器会根据参数的静态类型决定使用哪个重载版本。

所有依赖静态类型来决定方法执行版本的分派动作成为静态分派,静态分派发生在编译阶段。另外,虽然编译器能确定方法的重载版本,但很多情况下这个重载版本并不是唯一的,往往只能确定一个更合适的版本,比如:

public class Overload {

    public static void sayHello(Object arg) {
        System.out.println("hello Object");
    }

    public static void sayHello(int arg) {
        System.out.println("hello int");
    }

    public static void sayHello(long arg) {
        System.out.println("hello long");
    }

    public static void sayHello(Character arg) {
        System.out.println("hello Character");
    }

    public static void sayHello(char arg) {
        System.out.println("hello char");
    }

    public static void sayHello(char... arg) {
        System.out.println("hello char ...");
    }

    public static void sayHello(Serializable arg) {
        System.out.println("hello Serializable");
    }

    public static void main(String[] args) {
        sayHello('a');
    }
}

这段代码的输出结果是:

hello char

但它有一个很神奇的地方,假如注释掉方法 sayHello(char arg),那么它会输出:

hello int

继续注释下去,发现 ‘a’ 会按优先顺序依次被转型为 char、int、long、Character、Serializeable(Character 实现的接口之一)、Object、char…,可见变长参数的重载优先级是最低的,但注意,’a’ 不能转型为 Integer、Long 等类型。

不过,这种代码相当于“茴香豆的茴有几种写法”的研究,实际编程应尽量避免这种情况。

动态分派

动态分派和覆盖(Override,复写、重写)有着很密切的关联,比如:

public class DynamicDispatch {

    static abstract class Human {
        protected abstract void sayHello();
    }

    static class Man extends Human {
        @Override
        protected void sayHello() {
            System.out.println("man say hello");
        }
    }

    static class Woman extends Human {
        @Override
        protected void sayHello() {
            System.out.println("woman say hello");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }
}

运行结果:

man say hello
woman say hello
woman say hello

man、woman 这两个对象是将要执行的 sayHello() 方法的所有者,称为接收者。

使用 javap 分析这段代码:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class org/fenixsoft/polymorphic/DynamicDispatch$Man
         3: dup
         4: invokespecial #3                  // Method org/fenixsoft/polymorphic/DynamicDispatch$Man."<init>":()V
         7: astore_1
         8: new           #4                  // class org/fenixsoft/polymorphic/DynamicDispatch$Woman
        11: dup
        12: invokespecial #5                  // Method org/fenixsoft/polymorphic/DynamicDispatch$Woman."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method org/fenixsoft/polymorphic/DynamicDispatch$Human.sayHello:()V
        20: aload_2
        21: invokevirtual #6                  // Method org/fenixsoft/polymorphic/DynamicDispatch$Human.sayHello:()V
        24: new           #4                  // class org/fenixsoft/polymorphic/DynamicDispatch$Woman
        27: dup
        28: invokespecial #5                  // Method org/fenixsoft/polymorphic/DynamicDispatch$Woman."<init>":()V
        31: astore_1
        32: aload_1
        33: invokevirtual #6                  // Method org/fenixsoft/polymorphic/DynamicDispatch$Human.sayHello:()V
        36: return

可以发现,第 17、33 条语句中的参数都是同一个,但最终执行的目标方法不同,这是 invokevirtual 的特性决定的,invokevirtual 指令的运行时解析过程大致可以分为一下几个步骤:

1) 找到操作数栈顶的第一个元素所指向的对象的实际类型 C

2) 如果在 C 中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,通过则返回直接引用,查找结束;否则抛出 java.lang.IllegalAccessError 异常

3) 否则,按照继承关系从下往上一次对 C 的各个父类进行第 2 步的搜索和验证过程

4) 如果没有找到,则抛出 java.lang.AbstractMethodError 异常

单分派与多分派

方法的接收者与方法的参数统称为方法的宗量。例子:

public class Dispatch {

    static class QQ {}

    static class _360 {}

    public static class Father {
        public void hardChoice(QQ arg) {
            System.out.println("father choose qq");
        }

        public void hardChoice(_360 arg) {
            System.out.println("father choose 360");
        }
    }

    public static class Son extends Father {
        public void hardChoice(QQ arg) {
            System.out.println("son choose qq");
        }

        public void hardChoice(_360 arg) {
            System.out.println("son choose 360");
        }
    }

    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new _360());
        son.hardChoice(new QQ());
    }
}

运行结果:

father choose 360
son choose qq

在这个例子中,有两个分派过程。一个是编译阶段编译器的选择过程,也就是静态分派的过程,此时选择目标方法的依据有两点:对象的静态类型、方法参数的类型,因此是静态多分派。而运行阶段虚拟机的选择过程,也就是动态分派的过程,此时选择目标方法的依据只有一个:对象的实际类型,因此是动态单分派。

因此,今天的 Java 是一门静态多分派,动态单分派的语言。

虚拟机动态分派的实现

由于动态分派是非常频繁的动作,因此基于性能的考虑,大部分虚拟机实现都会进行优化,最常用的是稳定优化,为类在方法区中简历一个虚方法表(与此对应的,invokeinterface 执行时也会用到接口方法表),使用虚方法表来代替元数据查找以提高性能。虚方法表结构如图:

虚拟机字节码执行引擎

虚拟机除了使用方法表之外,在条件允许的情况下,还会使用内联缓存和基于类型继承关系分析技术的守护内联两种非稳定的激进优化手段来获得高性能。

动态类型语言支持

Java 虚拟机的字节码指令集数量从 Sun 发布第一款 Java 虚拟机到 JDK 7 之间,一直没发生任何变化,直到 JDK 7,字节码指令集终于迎来了第一位新成员——invokedynamic,这条指令的增加是 JDK 7 实现动态类型语言支持而进行的改进之一,也是为 JDK 8 可以顺利实现 Lambda 表达式做技术准备。

动态类型语言的关键特征是它的类型检查的主体过程是在运行期,而不是编译期。

Java 虚拟机的愿景之一是支持 Java 以外如 Groovy 这样的动态类型语言,JDK 1.7 以前也能实现类似的效果,但会带来额外的性能或内存开销,因此在 Java 虚拟机层面上提供动态类型的语言的直接支持就成为了 Java 语言平台的发展趋势之一。

JDK 1.7 加入了 java.lang.invoke 包,这个包的目的是在之前单纯依靠符号引用来确定调用的目标方法这种方式以外,提供一种新的动态确定目标方法的机制,称为 MethodHandle。例子:

/**
* JSR 292 MethodHandle基础用法演示
* @author zzm
*/
public class MethodHandleTest {

    static class ClassA {
        public void println(String s) {
            System.out.println("ClassA: " + 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 mt = MethodType.methodType(void.class, String.class);
        // lookup()方法来自于 MethodHandles.lookup
        // 作用是在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄
        // 因为这里调用的是一个虚方法,方法第一个参数是隐式的,代表该方法的接收者 - this
        // this 这个参数以前是放在参数列表中进行传递,现在提供了 bindTo() 方法来完成这件事情
        return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
    }

}

虽然用反射也能实现相同的效果,但 MethodHandle 和反射还是有区别的:

1) Reflection 是模仿 Java 代码层次的方法调用,MethodHandle 则是在模拟字节码层次的方法调用,MethodHandle.lookup 中的 3 个方法 findStatic、findVirtual、findSpecial 对应于 invokestatic、invokevirtual & invokeinterface、invokespecial 这几条字节码指令。

2) Reflection 中的 java.lang.reflect.Method 对象远比 MethodHandle 机制中的 java.lang.invoke.MethodHandle 对象所包含的信息多

3) 由于 MethodHandle 是对字节码的方法指令调用的模拟,所以虚拟机在这方面做的各种优化,理论上 MethodHandle 也可以采用类似的思路去支持(目前还不完善),而通过反射去调用方法则不行

4) 最关键的是,Reflection API 是为 Java 语言服务的,而 MethodHandle 则可服务于所有 Java 虚拟机之上的语言

invokedynamic 指令与 MethodHandle 机制的作用是一样的,都是为了解决 invoke 指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到用户代码之中,让用户有更高的自由度。invokedynamic 的第一个参数是 CONSTANT_InvokeDynamic_info 常量,从这个常量可以得到 3 项信息:引导方法(Bootstrap Method)、方法类型(MethodType)和名称。引导方法有固定的参数,返回值是 java.lang.invoke.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);
    }

    // 生成 testMethod
    public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable {
        return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt));
    }

    // 生成 BootstrapMethod 对应的 MethodType
    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);
    }

    // 生成 BootstrapMethod
    private static MethodHandle MH_BootstrapMethod() throws Throwable {
        return lookup().findStatic(InvokeDynamicTest.class, "BootstrapMethod", MT_BootstrapMethod());
    }

    // 调用 testMethod
    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();
    }
}

invokedynamic 与其它 4 条 invoke 指令最大的差别是它的分派逻辑不由虚拟机决定,而是由程序员决定。

猜你喜欢

转载自blog.csdn.net/u011330638/article/details/82729073