深入理解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
这是因为字段不支持多态性。这段代码的执行过程可以屡一下
- 执行
Father gay = new Son();
方法,子类构造方法会调用父类的构造方法。 - 所以会走进了public Father()构造方法,在调入public Father()方法之前,会执行
public int money = 3;
进行初始化 - 走进
public Father()
构造方法,执行money = 2;
进行赋值。 - 继续执行Father()中的
showMeTheMoney();
方法,但此时实际类型为Son,所以会走进Son类的public void showMeTheMoney()
- 因为son类此时的money并没有初始化赋值,所以只有默认值0,所以会打印
I am Son, i have $0
- 走完
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|【JVM】JVM08(java内存模型解析[JMM])
- jvm|JVM调优(线上 JVM GC 频繁耗时长,出现 LongGC 告警,这次排查后想说:还有谁(...))
- java内存区域与内存溢出异常
- JVM|JVM优化(一)
- 自动内存管理机制
- JVM: 使用 jstack 命令找出 cpu 飙高的原因
- java|JVM之字节码如何在jvm流转
- jvm|从栈帧看字节码是如何在 JVM 中进行流转的
- java|[NIO和Netty] NIO和Netty系列(二): Java Reference详解
- 生活|jrebeleclipse/tomcat 使用方法