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


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

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

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

上图展示的就是一个线程内的栈帧中的具体内容,接下来将会具体讲述栈帧中的具体区域。
栈帧的各个区域 局部变量表
局部变量表是一组变量值存储空间,存储方法的参数和方法内部定义的局部变量。局部变量表中的最小存储单位为容量槽,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); }

【深入理解java虚拟机系列第三版读后小记:(十二)运行时栈帧数据结构】运行结果需要读者思考一番
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 的静态分派是多分派,动态分派为单分派。
总结 本文详细了介绍了栈帧结构以及方法的执行过程,包括多态性的介绍和实现。

    推荐阅读