Chapter 8.1-8.2 - 运行时栈帧结构
Created by : Mr Dk.
2020 / 01 / 30 21:32 🧨🧧
Ningbo, Zhejiang, China
8.1 概述
关于 Java 虚拟机与物理机
- 物理机的执行引擎直接建立在 CPU、缓存、指令集和 OS 层面上
- 虚拟机的执行引擎由软件自行实现,不受体系结构限制
JVM 在执行字节码时,分为:
- 解释执行 (由解释器执行)
- 编译执行 (通过即时编译器产生本地代码)
8.2 运行时栈帧结构
JVM 以函数 (方法) 作为最基本的执行单元。栈帧 (Stack Frame) 是支持虚拟机进行函数调用和函数执行背后的数据结构,位于运行时数据区的 虚拟机栈 中,包含函数的:
- 局部变量表
- 操作数栈
- 动态链接
- 函数返回地址
栈帧需要分配的内存在编译时就已经确定了,不会受到程序运行时变量数据的影响。在活动线程中,只有位于栈顶的函数是正在运行的,只有位于栈顶的栈帧才是生效的,被称为 当前函数 和 当前栈帧。执行引擎运行的字节码指令只对当前栈帧进行操作。
8.2.1 局部变量表 (Local Variables Table)
存放 函数参数 和 函数内部定义的局部变量。在编译出 Class 文件时,函数的 Code 属性中就已经确定了局部变量表的最大容量。局部变量表的容量以 变量槽 (Variable Slot) 为最小单位。每个变量槽可以存放:
boolean
byte
char
short
int
float
reference
- 对象引用
- JVM 能根据引用找到对象在 Java Heap 中数据的起始地址
- JVM 能根据引用找到对象所属的类在方法区中存储的类信息
returnAddress
JVM 明确规定 long
和 double
需要占用两个连续的变量槽。局部变量表建立在线程堆栈中,属于线程私有数据,没有线程安全问题。JVM 通过索引定位的方式使用局部变量表,当一个函数被调用时,JVM 使用局部变量表来完成参数传递。如果执行的是实例方法,局部变量表第 0
位默认用于传递函数所属对象实例的引用,即隐含参数 this
;其余参数按照参数表顺序排列,占用 1
开始的局部变量槽。参数表分配完毕后,再按函数体内部变量定义的顺序和作用于分配其余变量槽。
为节省栈帧使用的内存空间,局部变量表中的变量槽是可以重用的。若当前字节码 PC 的值已经超出了变量作用域,那么这个变量槽就可以给其它变量使用。
关于手动将对象设置为
null
值是否有利于 GC,这种操作在极特殊情形下有用:
- 对象占用内存大
- 栈帧长时间无法被回收
- 函数调用次数达不到 JIT 的编译条件
在实际情况中,即时编译是 JVM 执行代码的主要方式,赋
null
的操作在经过即时编译优化之后会被消除,因而没有意义。
与类变量在准备阶段和初始化阶段的二度赋值不同,局部变量没有赋初始值是绝对不能使用的。编译器会检查到这一点。
8.2.2 操作数栈 (Operand Stack)
与局部变量表一样,操作数栈的最大深度在编译时被写入 Code 属性中。32-bit 数据类型的栈容量为 1;64-bit 数据类型的栈容量为 2。Javac 编译器的数据流分析保证了函数执行的任何时候,操作数栈的深度都不会超过最大深度。编译器保证操作数栈中的元素与字节码指令序列严格匹配,在类的校验阶段还会再次验证这一点。
两个栈帧在概念上是独立的,但在实现上,JVM 会使两个栈帧重叠。下面栈帧的操作数栈与上面栈帧的局部变量表重叠:
- 节省空间
- 函数调用时直接共用数据,无须进行额外的参数复制传递
8.2.3 动态链接
每个栈帧都包含一个指向常量池中该栈帧所属函数的引用。字节码中的 函数调用指令 以常量池中指向函数的符号引用作为参数
- 符号引用在类加载阶段或第一次使用时转化为直接引用 - 静态解析
- 符号引用在每一次运行期间都转化为直接引用 - 动态链接
8.2.4 方法返回地址
- 正常调用完成
- 可能会有返回值传递给上层的函数调用者
- 异常调用完成
- 函数执行过程中遇到了异常
- 本函数的异常表中没有搜索到匹配的异常处理器,方法退出
- 不会给调用者提供任何返回值
函数退出后,必须返回最初方法被调用时的位置,继续执行。函数退出的过程实际上等同于把当前栈帧出栈。