深入理解JVM--解析、分派、重载与重写

运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,是方法区中虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息。每个方法的调用与退出,都对应着一个栈帧从虚拟机栈入栈与出栈。

局部变量表存储了方法的入参和内部的局部变量,超过作用域的局部变量存储空间可以被其他变量复用;操作数栈用于方法内部的相关计算使用;动态链接用于将栈帧与方法区中具体的方法关联起来(静态关联或者从众多重载和重写版本中动态关联);方法返回地址分为两类,一类是正常退出指令,一类是异常退出。

方法调用

方法调用最关键的问题就是要确定具体调用哪个版本的方法。在Java虚拟机里共有5条方法调用字节码指令:

  • invokestatic:调用静态方法
  • invokespecial:调用实例构造方法,私有方法和父类方法
  • invokevirtual:调用虚方法
  • invokeinterface:调用接口方法,在运行时确定一个实现此接口的对象
  • invokedynamic
静态解析

通过invokestatic和invokespecial调用的方法在编译器就可以确定调用版本,在类加载的解析阶段,相应的符号引用会被替换为方法的直接引用,这个过程即为静态解析。

对于静态方法来说,它是与类相关的概念,而不是与对象相关的概念。静态方法无法被重写,编译器允许父子类中定义了两个同名的静态方法,但是这两个静态方法分别属于父子类,而不存在重写关系;静态方法可以重载,但仍然可以在编译期唯一确定一个调用版本。(下文再解释,为什么即使重载也可以在编译期确定唯一调用版本)

对于私有方法来说,不具备外部访问条件,即使重载,也可以在编译器确定唯一的调用版本。

分派

根据条件在多个方法版本中选择匹配方法即为分派。主要通过invokevirtual和invokeinterface指令调用。

通常,在B extends A, A a = new B();中,对于变量a,A称为a的静态类型,B称为a的实际类型。

  • 静态分派:通过变量静态类型来定位方法版本,在重载中应用
  • 动态分派:通过变量实际类型来定位方法版本,在重写中应用

这里静与动的意思分别指的是编译期和运行时,变量定义时的静态类型在编译期是可知的,而变量的实际类型是编译期不可确定的。如以下情况:

B,C extends A
A a = null;//静态类型可知
if(condition){
    a = new B();
}else{
    a = new C();
}
//实际类型要在运行时才能确定
静态分派

通过一段代码来说明:

public class Dispatch {

    public static void test(Chinese c){

    }

    public static void test(People p){

    }

    static class People {

    }

    static class Chinese extends People {

    }

    static class English extends People {

    }

    public void speak(People p) {
        System.out.println("undefined");
    }

    public void speak(Chinese c) {
        System.out.println("人");
    }

    public void speak(English e) {
        System.out.println("people");
    }


    public static void main(String[] args) {
        Dispatch d = new Dispatch();
        //定义变量p,其静态类型为People,动态类型也为People。
        People p = new People();
        //同上
        Chinese c = new Chinese();
        //同上
        English e = new English();
        // speak方法为Dispatch上的重载方法,通过传入不同静态类型的变量,看编译期如何选择
        d.speak(p);
        //将c的静态类型强制转换为People
        d.speak((People)c);
        d.speak(c);
        //将e的静态类型强制转换为People
        d.speak((People)e);
        d.speak(e);

        //将静态类型为People的p的实际类型转换为Chinese
        p = c;
        d.speak(p);
        p = e;
        d.speak(p);

        Dispatch.test(p);//静态方法的选择
    }
}

输出为:

undefined//静态类型为People
undefined//c的静态类型被强制转换为People//静态类型为Chinese
undefined//e的静态类型被强制转换为People
people//静态类型为English
undefined// 不论实际类型怎么变化,p的静态类型始终为People
undefined
undefined//静态方法的选择一样

可以看到,在选择重载方法的版本时,编译器依据的是传入参数的静态类型(当然也与入参的数量有关),而与其实际类型无关。静态类型是编译期可知的,所以,在静态方法和私有方法的重载中,在编译期也是可以确定调用版本的。

动态分派

还是上文中的代码,添加如下部分:

static class DispatchSon extends Dispatch {
        public void speak(People p) {
            System.out.println("son undefined");
        }

        public void speak(Chinese c) {
            System.out.println("son 人");
        }

        public void speak(English e) {
            System.out.println("son people");
        }
    }

main方法:

public static void main(String[] args) {
        Dispatch d = new Dispatch();
        People p = new People();
        Chinese c = new Chinese();
        English e = new English();

        d.speak(p);
        d.speak(c);
        d.speak(e);
        //将变量d的实际类型改变为DispatchSon类型
        d = new DispatchSon();
        d.speak(p);
        d.speak(c);
        d.speak(e);
    }

输出:

undefined
人
people
//改变实际类型后调用了子类重写的方法
son undefined
son 人
son people

可以看到,在选择重写的方法时,依据是变量(对象)的实际类型,而不是静态类型。

示例代码字节码

接下来我们在字节码层面看一下,为什么会像上文那样选择,字节码很长,省略了部分代码,将main方法改为:

public static void main(String[] args) {
        Dispatch d = new Dispatch();
        People p = new People();
        Chinese c = new Chinese();

        d.speak(p);
        d.speak((People) c);
        d.speak(c);

        p = c;
        d.speak(p);

        Dispatch.staticSpeak(p);

        d.speak(p);
        d = new DispatchSon();
        d.speak(p);

    }

部分字节码如下:

// class version 51.0 (51)
// access flags 0x21
public class Dispatch {
  // access flags 0x1
  public speak(Dispatch$People) : void
   L0
    LINENUMBER 53 L0
    GETSTATIC System.out : PrintStream
    LDC "undefined"
    INVOKEVIRTUAL PrintStream.println (String) : void
   L1
    LINENUMBER 54 L1
    RETURN
   L2
    LOCALVARIABLE this Dispatch L0 L2 0
    LOCALVARIABLE p Dispatch$People L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1
  public speak(Dispatch$Chinese) : void
   L0
    LINENUMBER 57 L0
    GETSTATIC System.out : PrintStream
    LDC "\u4eba"
    INVOKEVIRTUAL PrintStream.println (String) : void
   L1
    LINENUMBER 58 L1
    RETURN
   L2
    LOCALVARIABLE this Dispatch L0 L2 0
    LOCALVARIABLE c Dispatch$Chinese L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x9
  public static main(String[]) : void
   L0
    LINENUMBER 79 L0
    NEW Dispatch
    DUP
    //构造方法调用指令invokespecial
    INVOKESPECIAL Dispatch.<init> () : void
    ASTORE 1

   L3
    LINENUMBER 83 L3
    ALOAD 1: d
    ALOAD 2: p
    //普通方法调用invokevirtual
    INVOKEVIRTUAL Dispatch.speak (Dispatch$People) : void
   L4
    LINENUMBER 84 L4
    ALOAD 1: d
    ALOAD 3: c
    //这里对应强制转换,可以看到此处调用时speak方法的入参类型为People类型
    INVOKEVIRTUAL Dispatch.speak (Dispatch$People) : void
   L5
    LINENUMBER 85 L5
    ALOAD 1: d
    ALOAD 3: c
    INVOKEVIRTUAL Dispatch.speak (Dispatch$Chinese) : void
   L6
    LINENUMBER 87 L6
    ALOAD 3: c
    ASTORE 2: p
   L7
    LINENUMBER 88 L7
    ALOAD 1: d
    ALOAD 2: p
    INVOKEVIRTUAL Dispatch.speak (Dispatch$People) : void
   L8
    LINENUMBER 90 L8
    ALOAD 2: p
    //静态方法调用指令invokestatic
    INVOKESTATIC Dispatch.staticSpeak (Dispatch$People) : void
   L9
    LINENUMBER 92 L9
    ALOAD 1: d
    ALOAD 2: p
    //实际类型为Dispatch时的调用
    INVOKEVIRTUAL Dispatch.speak (Dispatch$People) : void
   L10
    LINENUMBER 93 L10
    NEW Dispatch$DispatchSon
    DUP
    INVOKESPECIAL Dispatch$DispatchSon.<init> () : void
    ASTORE 1: d
   L11
    LINENUMBER 94 L11
    ALOAD 1: d
    ALOAD 2: p
    //实际类型为DispatchSon时的调用
    INVOKEVIRTUAL Dispatch.speak (Dispatch$People) : void
   L12
    LINENUMBER 96 L12
    RETURN
   L13
    LOCALVARIABLE args String[] L0 L13 0
    LOCALVARIABLE d Dispatch L1 L13 1
    LOCALVARIABLE p Dispatch$People L2 L13 2
    LOCALVARIABLE c Dispatch$Chinese L3 L13 3
    MAXSTACK = 2
    MAXLOCALS = 4
}

在字节码中实际类型不同的调用字节码却是相同的,如何根据实际类型调用重写的方法呢?答案在invokevirtual指令的内部执行逻辑里:invokevirtual指令内部的第一步就是确定调用方法的变量的实际类型,将方法调用的符号引用解析到实际类型的直接引用上。

深入理解Java虚拟机

猜你喜欢

转载自blog.csdn.net/john_lw/article/details/80046762
今日推荐