深入理解java虚拟机系列第三版读后小记:(十二)运行时栈帧数据结构

深入理解java虚拟机系列第三版读后小记: 十二 运行时栈帧数据结构

  • 前言
    • 栈帧的各个区域
      • 局部变量表
      • 操作数栈
      • 动态连接
      • 方法返回地址
      • 附加信息
    • 方法调用
      • 解析
      • 分派
        • 静态分派
        • 动态分派
        • 单分派与多分派
  • 总结

前言

之前提到java内存布局的时候提到过虚拟机栈,其中虚拟机栈里存储的元素就是栈帧。栈帧存储了局部变量表,操作数栈,动态连接和方法返回地址等信息,每一方法从调用开始至执行结束的过程,就是虚拟机栈中的一个栈帧从入栈到出栈这个过程。
深入理解java虚拟机系列第三版读后小记:(十二)运行时栈帧数据结构_第1张图片
上图展示的就是一个线程内的栈帧中的具体内容,接下来将会具体讲述栈帧中的具体区域。

栈帧的各个区域

局部变量表

局部变量表是一组变量值存储空间,存储方法的参数和方法内部定义的局部变量。局部变量表中的最小存储单位为容量槽,32位虚拟机中,一个容量槽能存储的类型为int,short,boolean,float,byte,char,reference,returnAddress类型,前六种就java基本类型,reference是对象实例的引用类型,至于最后一个存储指向一些字节码的地址,已经很少用到。在64位虚拟机中,根据高低位分配两个连续的容量槽来存储,新增了long,double两种类型。
局部变量表中的变量不像类变量一样存在两个阶段赋值,第一阶段准备赋初始值,第二阶段赋值用户定义的值,局部变量在初始化阶段创建好就赋上用户定义的值。

操作数栈

又称操作栈,先进后出的数据结构,栈的深度在编译期间就已确定好。主要就是根据字节码指令进行出入栈的操作。

动态连接

每个栈帧中包含一个指向运行时常量池中的该方法的引用,持有这引用就是为了在方法中支持动态连接。之前提到过class的常量池中存有大量符号引用,这些符号引用在类加载或第一次使用转为为直接引用称为静态引用,而在运行期每次都转为直接引用称为动态引用。

方法返回地址

方法执行后,退出方式只有两种

  • 执行引擎遇到返回字节码指令,就将返回值推给上层调用者,这种称为正常调用完成。
  • 另一种就是方法执行中遇到了无法处理的异常,该异常在异常表中没有对应的异常处理器,就会退出方法。
    方法退出相当于将当前栈帧进行出栈

附加信息

指规范中没有提到的一些附加信息,如调试,性那相关信息等,该附加信息由各自的jvm实现机实现。

方法调用

方法调用并不是说具体发方法执行过程,方法调用的唯一任务就是确认被调用方法的版本(即确定调用哪个方法)
之前提到过符号引用转换为直接引用有静态连接和动态连接,所以确定具体执行调用某个方法就变的相对复杂。

解析

即编译期可知,运行期不变,在加载阶段,即将常量池的符号引用转为直接引用,符合这类条件的方法就只有静态方法和私有方法,前者与类型关联,后者外部不可访问,所以这两方法的无法通过继承或别的方式重写出其他版本,所以放在加载阶段转化直接引用没有问题。

分派

分派指方法的调用,分派是多态性的表现。

静态分派

静态分派指重载,并不是常说的静态语义,分派本身就是动态性的体现。

public class Test {

    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();
        Test sr = new Test();
        sr.sayHello(man);
        sr.sayHello(woman);
    }
}

执行结果有经验的读者知道

hello,guy!
hello,guy!

把main方法中的Human称为静态类型,或叫外观类型,对应的
Man类型称为实际类型。静态类型是确定可知的,而实际类型是真正运行期才知道调用的,在编译期间,就确定静态类型Human,所已重载方法就选择了sayHello(Human guy),这就是静态分派。
所有依赖静态类型决定的方法执行的分派动作,都称为静态分派,最常用的就是重载。

动态分派

动态分派代表着多态的另一重要特性重写。
同样,先看个小例子

 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

动态分派了解过静态分派,有过多态性的基础读者都很清楚。其jvm实现的过程如下

  • 找到当前栈顶的实际类型,Man
  • 在这个类(Man)中找到与常量中的描述符和名称都相符的方法,即诚谢的方法。进行权限访问,有权即返回方法引用,无权抛异常
  • 没找到的话,就根据继承关系从下往上找,父类中进行第二步
  • 最后都没找到,抛异常。

再看个例子

static class Father {
public int money = 1;
public Father() {
money = 2;
showMeTheMoney();
}
public void showMeTheMoney() {
System.out.println("I am Father, i have $" + money);
}
}
static class Son extends Father {
public int money = 3;
public Son() {
money = 4;
showMeTheMoney();
}
public void showMeTheMoney() {
System.out.println("I am Son, i have $" + money);
}
}
public static void main(String[] args) {
Father gay = new Son();
System.out.println("This gay has $" + gay.money);
}

运行结果需要读者思考一番

I am Son, i have $0
I am Son, i have $4
This gay has $2

这是因为字段不支持多态性。这段代码的执行过程可以屡一下

  1. 执行Father gay = new Son(); 方法,子类构造方法会调用父类的构造方法。
  2. 所以会走进了public Father()构造方法,在调入public Father()方法之前,会执行public int money = 3; 进行初始化
  3. 走进public Father() 构造方法,执行money = 2;进行赋值。
  4. 继续执行Father()中的showMeTheMoney();方法,但此时实际类型为Son,所以会走进Son类的public void showMeTheMoney()
  5. 因为son类此时的money并没有初始化赋值,所以只有默认值0,所以会打印I am Son, i have $0
  6. 走完public Father()后,走进Son的构造方法,重复之前2-5的步骤

单分派与多分派

方法的接受者与方法的参数统称为方法的宗量,根据分派基于宗量的选择,可以判断为单分派还是多分派。一个宗量对方法选择的是单分派,多个宗量对方法选择的是多分派,可能有些难以理解,看个例子

 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

解析一下
静态分派father.hardChoice(new _360());中确定执行哪个方法的因素,一个是静态类型,是Father还是子类Son,另一个是参数类型_360,还是QQ,所以会在常量池产生两个字符引用指向Father类的下两个hardChoice方法,由两个宗量确定,所以可知java 的静态是多分派。

接下来看重写,动态分派拿到的类型是实际类Son,执行方法的参数son.hardChoice(new QQ());编译期间就可以确定,所以就一个宗量,是单派的。
所以得到的结论是java 的静态分派是多分派,动态分派为单分派。

总结

本文详细了介绍了栈帧结构以及方法的执行过程,包括多态性的介绍和实现。

你可能感兴趣的:(jvm)