栈帧中都有啥东西
大纲
前言
我的所有文章同步更新与Github–Java-Notes,想了解JVM,HashMap源码分析,spring相关,剑指offer题解(Java版),可以点个star。可以看我的github主页,每天都在更新哟。
邀请您跟我一同完成 repo
栈帧是虚拟机栈的一个单位,之前讲解了运行时数据区和类加载的过程,现在我们看下虚拟机中栈帧都是啥样子的,这个应该算是运行时数据区(JVM内存结构的补充),如果不了解可以参考我的这篇博文 JVM内存结构
运行时栈帧结构
运行时栈帧中存储了以下内容
- 局部变量
- 操作数栈
- 动态链接
- 返回地址
- 附加信息
- ….
每一个方法的调用开始和结束都是栈的压入(入栈)和弹出(出栈)的过程
局部变量表
是什么
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
大小是编译的时候写进了字节码里面的,在Code属性中的max_local属性,即下面的local
有什么
局部变量表的容量以变量槽(Variable Slot)为最小单位,虚拟机中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性的说到每个Slot都应该能存放一个下面8种类型的其中一个。(如果是long或者double这种64位的数据类型,则需要两个Slot)
- boolean
- byte
- char
- short
- int
- float
- reference
- returnAddress
前六种应该不用说,是基本的数据类型,reference是啥呢
reference
reference是一个对象实例的引用
作用:
- 从此引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引
- 从此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型对象(因为类信息在方法区中存储)
returnAddress
为字节码指令jsr、jsr_w和ret提供的,指向了一条字节码指令的地址
已经很少见了。
注意
局部变量表中的局部变量和之前将类加载的时候的类变量(static修饰)不一样,他没有所谓的"准备阶段",所以没有设置初始值的阶段。不知道的可以参考我类加载这篇文章,看了准备阶段,应该就知道了。类加载过程
所以我们在写程序的时候这样写,对比你就知道了
其他类型零值
Slot复用
不使用的对象,应当手动赋值为null
为了尽可能节省栈空间,局部变量表的Slot可以复用。方法体中定义的变量,其作用域并不一定覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。
不过这样的做法,会有一些缺点,我们来看下面的代码示例
public class Test2 {
public static void main(String[] args) {
byte[] placeholder = new byte[64 * 1024 * 1024];
System.gc();
}
}
我们通过配置虚拟机参数-verbose:gc
来打印垃圾回收的结果
我们看到他并没有回收。
我们修改一下代码
public class Test2 {
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
System.gc();
}
}
他还是没有进行回收,按理说 placeHolder 的作用域只在花括号中,在执行gc方法的时候,他就已经不可能用了,算是已经"死"了的对象了,为什么没有回收呢?
我们再修改一下
public class Test2 {
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
int a = 0;
System.gc();
}
}
我们看到,这次垃圾回收器工作了,为什么呢?
placeholder 能否被回收的根本原因是:局部变量表的Slot是否还存有关于placeholder数组对象的引用。
第一次修改中,代码虽然离开了该变量的作用域,但是在此之后,没有任何对局部变量表的读写操作,该变量原本占用的Slot还没有被任何其他变量复用,所以作为GC Root 一部分的局部变量表仍然保持着对他的关联(不了解什么可以作为GC Root的话,可以参考我的这篇文章 JVM垃圾回收)
而第二次,则改变了上面的这种情况
所以当遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用大量内存、实际上已经用不到的变量,应当手动设置其为null。
很多工具类都有这个操作,比如 ArrayList和Stack中的remove方法,你也可以找下其他的工具类中的方法,看是否有此类操作
操作数栈
操作数栈记录了一个方法执行过程中的字节码指令,他往操作数栈中进行入栈和出栈
大小在编译的时候也已经确定了,字节码文件中的Code属性中的max_stacks数据项,即下图的stack
当一个方法刚执行的时候,操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中入栈和出栈。
动态连接
每一个栈帧都包含一个指向运行时常量池中该栈帧所属的方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
如果你看了字节码文件构成和类加载过程,你应该知道,字节码文件中有很多符号引用。
这些符号引用一部分会在类加载的解析阶段或者第一次使用的时候转化为直接引用,这种转化称为静态解析
另一部分会在运行期间转化为直接引用,这部分称为动态连接
动态连接会在这篇文章 方法调用 中讲解
返回地址
一个方法执行后,只有两种方法可以退出:
- return,正常退出
- 异常,并且不在该方法中处理
方法退出时可能的操作:
- 恢复上层方法的局部变量表和操作数栈
- 把返回值(如果有的话)压入调用者战争的操作数栈中
- 恢复PC计数器的值,以指向方法调用指令后面的一条指令
附加信息
这部分信息完全取决于具体的虚拟机实现,有的可能有,有的没有。
总结
- 栈帧中主要有4种信息
- 局部变量表
- 单位Slot
- Slot复写
- 操作数栈
- 方法执行时,入栈出栈各种字节码指令
- 动态连接
- 运行期间,将符号引用转化为直接引用
- 返回地址
- return
- 异常并且没有在该方法中处理
- ….
- 局部变量表