虚拟机栈
概述
由于不同平台CPU的架构不同,所以为了满足跨平台的要求,Java指令没有设计为基于寄存器的,而是根据栈来设计的。
栈是运行时的单位,堆是存储的单位
- 栈解决了程序的运行问题,即程序如何执行
- 堆解决数据的存储问题,即数据怎么放,放哪里
Java虚拟机栈
Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用,线程私有 。 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。
不同线程中所包含的栈帧不允许存在相互引用,即不可能在一个栈帧中引用另一个线程的栈帧。
Java 方法有两种返回函数的方式,一种是正常的函数返回,使用 return 指令;另 外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
栈帧结构
每个栈帧中存储着
- 局部变量表 (Local Variables)
- 操作数栈(Operand Stack)
- 动态链接(DynamicLinking) (或执行运行时常量池的引用)
- 方法返回地址(Return Adress) (或方法正常退出或异常退出的定义)
- 附加信息
并行下每个线程的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表和操作数栈决定的
局部变量表
局部变量表定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及 returnAddress 类型。
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的 Code 属性 maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的。
局部变量表中最基本的存储单位为Slot(变量槽),32位以内的类型只占用一个Slot(包括return Adress类型),64位的类型(long 和 double)占用两个Slot。JVM 会为局部变量表中的每一个 Slot 都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明新的局部变量就很可能会复用过期局部变量的槽位,从而达到节省资源的目的。
成员变量与局部变量的对比
按类中声明的位置分:成员变量(类变量,实例变量),局部变量
- 类变量:
位于方法区,在链接的准备阶段进行默认赋值,在初始化阶段进行显式赋值 - 实例变量:
位于堆,随着对象创建,在堆空间分配内存,进行默认赋值 - 局部变量:
位于栈帧,使用前必须进行显式赋值(人为初始化),否则无法使用
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接 引用的对象都不会被回收。
操作数栈
操作数栈,遵循后进先出原则。在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push) 和 出栈(pop),执行复制、交换、求和等操作。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的 Code 属性中,为 maxstack 的值。 栈中的任何一个元素都是可以任意的 Java 数据类型
- 32bit 的类型占用一个栈单位深度
- 64bit 的类型占用两个栈单位深度(long,double)
操作数栈并非采用访问索引的方式来进行数据访问的,而是通过标准的入栈和出栈操作来完成一次数据访问。
如果被调用的方法有返回值的话,其返回值会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic 指令。
在Java源文件编译到字节码文件中时,字面量(常量、字符串)、符号引用(方法引用)被保存在了class文件的常量池中。
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是将符号引用转换为调用方法的直接引用。 不同的方法,都可能调用同一个方法或常量,常量池的作用就是将这些方法或变量只存储一份,节省了空间,便于指令的识别。
方法返回地址
存放调用该方法的PC寄存器的值。
一个方法的结束,有两种:
方法正常退出时,调用者的PC计数器的值作为返回地址,而通过异常退出的,返回地址则要通过异常表来确定,栈帧中一般不保存这部分信息。
当一个方法执行后,会有两种方式退出这个方法
执行引擎遇到一个任意一个方法返回的字节码指令(return ),会有返回值传递给上层的方法调用者,简称正常完成出口。使用哪个返回指令需要根据方法返回值的实际数据类型而定。
在字节码指令中,返回指令包含 ireturn(当返回值是 boolean,byte,char,short 和 int 类型时使用),lreturn(Long 类型),freturn(Float 类型),dreturn(Double 类型),areturn。另外还有一个 return 指令为声明为 void 的方法,实例初始化方法,类和接口的初始化方法使用。
在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口。
方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置 PC 寄存器值等 ,让调用者方法继续执行下去。
附加信息
《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现。
参考
深入了解Java虚拟机 宋红康JVM笔记
|