虚拟机栈是程序运行的逻辑控制,虚拟机栈控制着方法的入栈与出栈,代表着方法的运行,本文来了解一下JVM中的虚拟机栈结构和功能;
1. Java虚拟机栈是什么?
Java虚拟机栈(Java Virtual Machine Stack)是运行时的单位,栈解决程序的运行问题,其通过栈帧的入栈出栈来实现代码的运行;
除此之外虚拟机栈还包括一下特点:
- 虚拟机栈是线程私有的;
- 生命周期和线程一致;
- 虚拟机栈不存在垃圾回收问题,但是存在**
OutOfMemoryError
和StackOverflowError
;** - JVM允许虚拟机栈的大小是动态的或固定不变的(HotSpot是固定不变的);
1.1 虚拟机栈中常见的异常
- 栈大小固定不变:常见StackOverFlowError异常(线程请求分配的栈容量超过虚拟机运行的最大容量);
- 栈大小可变:常见OutOfMemoryError异常(1. 尝试扩展无足够内存;2. 创建新线程无足够空间创建对应的虚拟机栈);
1.2 异常例子
【例 StackOverFlowError】
代码
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
配置
设置栈的大小: -Xss256k
运行结果
2. 栈中存储什么?
栈中存储的数据单位为栈帧,栈帧随着方法的运行而创建,栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
如下图所示,为虚拟机栈中的结构
栈帧有如下特点:
- 一条活动线程中,一个时间点上,只会有一个活动的栈帧,即只有当前栈帧(栈顶栈帧)是有效的;
- 当前栈帧对应着当前执行的方法;定义这个方法的类叫当前类;
- 执行引擎只对当前栈帧进行操作;
- 若当前方法调用了其他方法,则对应的新栈帧被创建出来,放入栈顶,成为新的当前栈帧;
示意图如下所示
3. 栈的运行原理
随着方法的调用,对应着栈帧的入栈和出栈,代码得以执行,栈的运行需要弄清以下几点内容:
- 不同线程中所包含的栈帧是不允许存在互相引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧;
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,使得前一个栈帧重新成为当前栈帧;
- Java方法有两种返回函数的方式,**一种是正常的函数返回,使用return指令,另外一种是抛出异常。**不管使用哪种方式,都会导致栈帧被弹出;
4. 栈的内部结构
每个栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)
- 动态链接(Dynamic Linking)
- 方法返回地址(Return Address)
- 一些附加信息
4.1 局部变量表(Local Variables)
局部变量表示一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,这些数据类型包括基本数据类型、对象引用以及returnAddress类型。
针对局部变量表,有如下几点需要注意:
- 局部变量表在线程的栈上,为线程私有数据,不存在数据安全性问题;
- 局部变量表所需的容量是在编译期间确定下来的,包括在方法的code属性的maximum local variables数据项中;
- 局部变量表的变量只在当前方法调用中有效;
- 局部变量表的容量以**变量槽(Variable Slot)**为最小单位;
4.1.1 Slot
slot的大致结构如下:
其中有几个需要关注的点:
- slot槽的索引从0开始,在构造函数和实例方法中index=0放置该对象的引用this;
- 32位以内的类型只占用一个slot(包括 byte、char、int、boolean、returnAdress等),64位类型(long和double)占用两个slot;
- 局部变量表中的变量槽是可复用的(当变量过了其作用域后);
- 局部变量表中的变量必须显式初始化;
- 局部变量表中的变量是重要的垃圾回收根节点;
4.2 操作数栈(Operand Stack)
操作数栈是一个用于存放指令执行中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈需需要弄清以下几点:
- 随着方法的执行被创建出来,刚开始为空的;
- 在方法编译是栈的最大深度已被计算出来,保存在max_stack中;
- 栈中可以是任意的Java数据类型(32位占一个栈单位深度,64位占两个栈单位深度);
- 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令;
- 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配(在编译和类加载的验证阶段会进行验证);
- Java虚拟机是基于栈的执行引擎,指的是操作数栈;
4.3 栈顶缓存技术
栈式指令架构更紧凑,但是完成一项操作需要更多的入栈出栈操作,意味着需要更多的内存读写次数,这会影响执行速度,为了缓解这个问题,HotSpot设计了栈顶缓存技术;
栈顶缓存(Top-of-Stack Cashing,Tos):将栈顶元素全部缓存在物理CPU的寄存器中,以此降低内存的读/写次数,提升执行引擎的执行效率;
4.4 动态链接(Dynamic Linking)
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
我们知道Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。
示意图如下所示:
4.5 方法返回地址(Return Adress)
- 存放调用改方法的pc寄存器的值;
- 一个方法的结束有两种方式(正常执行完成、出现未处理异常退出),无论何种方法退出,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址;
4.6 附加信息
附加信息指的是栈帧中携带与Java虚拟机实现相关的一些附加信息,例如对程序调式提供支持的信息;