代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。
概述
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的虚拟机栈的栈元素。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了。
因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。
局部变量表
我们在Java内存区域中在虚拟机栈提到 : 对于我们来说,主要关注的stack栈内存,就是虚拟机栈中局部变量表部分。
也就是说局部变量表在栈中起着举足轻重的作用。
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。
变量槽(Variable Slot)
局部变量表的容量以变量槽为最小单位,每个变量槽都可以存储32位长度的内存空间,例如boolean、byte、char、short、int、float、reference。
对于64位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的Slot空间,也就是相当于把一次long和double数据类型读写分割成为两次32位读写。
- reference(对象实例的引用)一般来说,虚拟机都能从引用中直接或者间接的查找到对象的以下两点 :
①在Java堆中的数据存放的起始地址索引。
②所属数据类型在方法区中的存储的类型数据。
整形 | 字节(b) | bit(位) |
---|---|---|
byte | 1 | 1*8 |
short | 2 | 2*8 |
int | 4 | 4*8 |
long | 8 | 8*8 |
浮点型 | 字节(b) | bit(位) |
---|---|---|
folat | 4 | 4*8 |
double | 8 | 8*8 |
Char类型 | 字节(b) | bit(位) |
---|---|---|
char | 2 | 2*8 |
boolean类型 | 字节(b) | bit(位) |
---|---|---|
boolean | 1 | 1*8 |
PS : 8bit=1b,1024b=1kb
在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用。(在方法中可以通过关键字
this
来访问到这个隐含的参数)。其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot。
- Slot复用
为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,也就是说当PC计数器的指令指已经超出了某个变量的作用域(执行完毕),那这个变量对应的Slot就可以交给其他变量使用。
优点 : 节省栈帧空间。
缺点 : 影响到系统的垃圾收集行为。(如大方法占用较多的Slot,执行完该方法的作用域后没有对Slot赋值或者清空设置null值,垃圾回收器便不能及时的回收该内存。)
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支付方法调用过程中的动态连接(Dynamic Linking)。
在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在每一次运行时期转化为直接引用。这部分称为动态连接。(关于这部分,后面会再继续分析)
方法返回地址
当一个方法开始执行后,只有2种方式可以退出这个方法 :
方法返回指令 : 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。
异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。
无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。
一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。
而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
操作数栈
操作数栈和局部变量表一样,在编译时期就已经确定了该方法所需要分配的局部变量表的最大容量。
操作数栈的每一个元素可用是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型占用的栈容量为2。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作。
例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的。
参数传递
索然两个栈帧作为虚拟机栈的元素是完全独立的,但是虚拟机会做出相应的优化,令两个栈帧出现一部分重叠。
如上图所示,栈帧的部分操作数栈与上一个栈帧的局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数复制传递。
算术运算
Java编译期输出的指令流,是基于栈的指令集合架构。
例如我们来看一段简单的算术代码 :
通过javap查看字节码指令 :
首先这段代码需要深度为2的操作数栈和4个Slot的局部变量空间。下面引用《深入理解Java虚拟机II》的图片来描述代码执行的结果,操作数栈和局部变量表的变化情况。
首先局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用
并且将100推入操作数栈。
然后将操作数栈的值出栈,并存储在局部变量表中。
将要运算的数值复制到操作数栈中(入栈)。
操作数栈运算(100+200)后出栈,再将结果(300)入栈。(因为还要继续运算)。
最后需要 乘运算,将数值300入栈。