栈帧是用于虚拟机进行方法调用和方法执行的数据结构。每一个栈帧的如栈和出栈过程代表了一个方法执行和结束的过程。
每一个栈帧包括局部变量表,操作数栈,动态链接,返回地址等等信息。栈帧的所有信息在编译代码阶段就已经确定了,不会收到运行时数据的影响。
1.局部变量表
每一个变量都会储存在Slot中,64位机器的Slot占64位,每个Slot都可以存储java数据类型和引用类型。
局部变量表的第0位默认是调用此方法的实例对象的引用,也就是this。其他参数按照顺序,占用从1开始的Slot。
为了节省栈帧空间,局部变量中的Slot是可以重用的,因为作用域不一定覆盖整个方法体。
对于局部变量表中变量的初始化,它不像类变量(static修饰)在类加载过程中的 “加载” 和 “初始化” 两次初始化,而是如果没初始化就不能使用。例如
class A {
public static int a;
public void haha {
int b;
System.out.printf(a);//1
System.out.printf(b);//2
}
}
//1编译通过而//2编译错误。
2.操作数栈
当一个方法开始执行的时候,不同于局部变量表从一开始分配好内存, 操作数栈一开始是空的,执行过程中,会有很多字节码指令往操作数栈中写入和提取内容,也就是执行入栈,出栈操作。例如加法操作就是通过操作数栈来进行的。
在概念模型中,两个栈帧是互相独立的,但是在大多数情况下都会做一些优化。
可以看的出来,下面的栈帧的部分操作数栈与上面的局部表重叠在一起。这也很容易理解,例如:
class A {
main方法{
int a = 1;
int b = 2;
int c = add(a , b); //执行到这里
}
public func int add(int a , int b){
return a + b;
}
}
main方法执行到add(a,b)方法的时候,add方法栈帧在上层,main函数栈帧在下层,a,b在下层main函数的操作数栈中,也在当前执行函数add方法中的局部变量中,这样进行方法调用时就可以共用一部分数据,无需进行额外的参数复制传递。
3.动态连接
每个栈帧中都包含一个指向运行时常量池中该栈帧中所属方法的引用,持有这个引用是为了支持动态连接。
动态连接是什么?
Class常量池中有很多符号引用,一部分 符号引用 会在类加载过程中转化成直接引用,叫做静态解析,另一部分会在运行时转化成直接引用,叫做动态引用。静态解析的函数类型有静态方法,构造函数,父类方法,私有方法等是“不可修改”的函数。
4.方法返回地址
退出方法有两种形式,正常退出和异常退出。
正常退出的情况下,方法会返回字节码命令(可能包含返回值)给调用者。而且返回地址是调用函数的PC计数器的值。
异常退出的情况下,是不会给调用者返回任何返回值的,而且,返回地址是要通过异常处理器来确定的。
因此,方法正常退出时,吧返回值压入调用者栈帧的操作数栈中,然后调整PC计数器的值指向下一条命令。异常退出是恢复上层方法中的局部变量表和操作数栈。