运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法放回地址等信息。
每个方法从调用考试至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
栈帧的概念结构如下图所示:
局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
局部变量表以变量槽(slog)为最小单位,每个slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,long和double数据类型占用两个slot
局部变量表中的slot是可以重用的
局部变量不像类变量那样存在“准备阶段”,也就是没有默认值。
操作数栈
32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2,。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入\提取内容,也就是出栈\入栈操作。
动态连接
每个栈帧都包含一个指向运行时常量池中改栈帧所述方法的引用,持有这个引用是为了支持方法调用过程在的动态连接。
一些符号引用会在类加载阶段就转化为直接引用,这种转化成为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分成为动态连接。
返回地址
遇到返回指令,把返回值传递给上层的方法调用者
方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,导致方法退出,没有返回值返回给调用者
方法调用
解析
在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期间是不可变的。这类方法的调用成为解析。
只要被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类
还有被final修饰的方法,也是可以确定的
分派
解析调用一定是一个静态的过程,分派调用可能是静态的也可能是动态的。
静态分派
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, getleman!");
}
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 staticDispatch = new StaticDispatch();
staticDispatch.sayHello(man);
staticDispatch.sayHello(woman);
}
}
输出结果为:
hello, guy!
hello, guy!
Human man = new Man();
“Human”成为变量的静态类型,后面的“Man”则成为变量的实际类型
实际类型变化
Human man = new Man();
man = new Woman();
静态类型变化
staticDispatch.sayHello((Man) man);
staticDispatch.sayHello((Woman) man);
重载时是通过参数的静态类型而不是实际类型作为判断的,并且静态类型是编译期可知的。
动态分配
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 say hello
woman say hello
重写是根据实际类型来调用的。
解析过程如下
- 找到实际类型,记作C
- 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则返回此引用
- 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索
- 否则,失败
虚拟机动态分配的实现
动态分配是非常频繁的动作,基于性能的考虑,大多数会在方法区中建立一个虚方法表,来代替元数据查找以提高性能。
一个例子的虚方法表结构如下图所示:
虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类没有被重写,那子类的虚方法表里面的地址入口和父类相同的方法的地址入口是一致的。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。
方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把类的方法表也初始化完毕。
参考
- 深入理解Java虚拟机[书籍]