简介
JVM以方法作为最基本的执行单元,
栈帧
则是用于支持JVM进行方法调用与方法执行背后的数据结构,同样它也是JVM运行时数据区中的虚拟机栈的栈元素。
1. 运行时栈帧结构
每个方法从调用开始到执行结束,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
栈帧存放了哪些信息?这些信息都存放在哪?
每个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外附加信息。在编译Java程序源码时,栈帧需要多大的局部变量表、多深的操作数栈就已经被分析计算出来并写入到方法表的Code属性中。
方法调用的时候哪个栈帧才是“当前栈帧”?
假如一个线程的方法调用链很长,同一时刻同一线程中,在调用堆栈的所有方法同时处于执行状态。在执行引擎的角度来说,在活动线程中,只有位于栈顶的方法才是运行的,只有位于栈顶的栈帧才是生效的,被称为当前栈帧
,与之关联的方法称为当前方法,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。
下图是虚拟机栈与栈帧的总体结构,接下来我们来一起学习栈帧中的局部变量表、操作数栈、动态连接、方法返回地址等各部分的作用和数据结构。
1.1 局部变量表
局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。
方法体里面的代码经过javac编译器处理之后,最终变为字节码指令存储在Code属性内,Code属性出现在方法表的属性集合中(接口或抽象类中的方法不存在Code属性),在Code属性max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。
如下列举了方法表结构及属性表中Code属性的结构:
方法表结构 | ||
---|---|---|
类型 | 名称 | 数量 |
u2 | access_flags(访问标志位) | 1 |
u2 | name_index(字段简单名称) | 1 |
u2 | descriptor_index(描述符) | 1 |
u2 | attributes_count(属性表存在个数) | 1 |
attribute_info(属性表) | attributes | attributes_count |
属性表结构 | ||
---|---|---|
类型 | 名称 | 数量 |
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
Code属性表结构 | ||
---|---|---|
类型 | 名称 | 数量 |
u2 | attribute_name_index 固定“Code” | 1 |
u4 | attribute_length 属性表长度 | 1 |
u2 | max_stack 操作数栈最大深度 | 1 |
u2 | max_locals 局部变量表所需存储空间 | 1 |
u4 | code_length 字节码长度 | 1 |
u1 | code 存储字节码指令的字节流 | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
局部变量表的容量max_locals
以变量槽(Slot)为最小单位,对于对于byte
、char
、float
、int
、short
、boolean
和returnAddress
等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double
和long
这两种64位的数据类型则需要两个变量槽来存放,最后一种为reference
类型。
局部变量表是线程安全?
局部变量表是建立在线程堆栈中的,属于线程私有的数据。
reference
数据类型用于存储什么?有什么作用?
reference
表示对一个对象实例的引用,《Java虚拟机规范》没有规定其长度也为明确应该是什么数据结构。虚拟机主要通过reference
做两件事:
- 根据引用直接或间接地查找对象在Java堆中数据存放的起始地址或索引。
- 根据引用直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息。
局部变量表具体存放哪些信息呢?
方法参数(包括实例方法中的隐式参数this
)、显示异常处理参数(try-catch()中定义的异常)、方法体中定义的局部变量。
方法调用时局部变量表的工作过程:
方法被调用时,JVM会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递:如果执行的是实例方法(没有static修饰的方法),局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,其余参数按照参数表顺序排列,占用从1开始的局部变量槽,参数表分配完毕之后,再根据方法内部定义的变量顺序和作用域分配其余的变量槽。
max_locals
的值是方法中的局部变量所占变量槽数之和吗?
答案是否定的,操作数栈和局部变量表直接决定该方法的栈帧耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存浪费。JVM的做法是将局部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量槽可以被其他局部变量所使用,Javac编译器
会根据变量的作用域来分配变量槽给各个变量使用,根据同时生存的最大局部变量数量和类型计算出max_locals
的大小。
1.2 操作数栈
也称为操作栈,它是一个后入先出(LIFO)栈。
操作数栈的最大深度在编译时写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。Javac编译器的数据流分析工作保证了在方法执行时,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。
方法调用时操作数栈的工作过程:
方法开始执行时,该方法的操作数栈是空的,执行过程中,字节码指令向操作数栈中写入和提取内容,即出栈和入栈操作。 例如字节码指令iadd,在运行时要求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,两个int值出栈并相加,然后将相加的结果重新入栈。
1.3 动态连接
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
1.4 方法返回地址
当方法开始执行后,只有两种方式退出这个方法:
- 当执行引擎遇到任意方法返回的字节码指令,称为“正常调用完成” 。
- 在方法执行的过程中遇到异常且异常没有在方法体内得到妥善处理,称为“异常调用完成 ”。
方法退出后,都必须返回到方法被调用时的位置,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。一般情况下,方法正常调动完成,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。 方法异常调用完成, 则反之。
参考
《深入理Java虚拟机》周志明