方法是JVM执行的基本单位,每个方法执行都对应一个独立的执行环境,栈帧就是这个执行环境的物理体现。栈帧是一个复合数据结构,封装了方法执行所需的所有运行时所需的数据:包括局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。
局部变量表
用于存储方法参数和方法内定义的局部变量,具有以下特点。
1、是一组变量值的存储空间(可以理解为数组),以变量槽(slot)为基本存储单位,按照方法参数->方法体内声明的变量->代码块内声明的变量的顺序进行分配,每个变量槽(slot)可存储一个32位数据,对于long、double两种64位的数据类型,会占据两个连续的slot;
2、对于实例方法,第一个slot(即数组下标为0)固定存储this引用;对于静态方法,没有this引用,第一个slot存储的是第一个方法参数;
3、局部变量表的大小在编译期就已经确定,存储在code属性的max_locals中,由于JVM会分析变量的作用域范围,当一个变量的作用域结束后,其占用的slot会被后续变量复用,这就导致max_locals中存储的不是所有局部变量数量的总和,而是同时存货的变量的最大数量,这样可以节省栈帧空间;
4、需要注意的是,局部变量表不存在类似类变量或者实例变量那样的"准备阶段",必须在使用前显示的进行初始化。对于native方法,局部变量表由本地方法自行管理。
各种数据类型占用的变量槽大小如下列表所示:
| 数据类型类别 | 具体类型 | Slot占用 | 存储方式 |
|---|---|---|---|
| 基本数据类型 | boolean, byte, char, short | 1 Slot | 扩展为32位存储 |
| int, float | 1 Slot | 直接存储 | |
| long, double | 2 Slots | 分高低位存储 | |
| 对象引用 | 类实例/数组/String等 | 1 Slot | 存储指向堆的指针(可能压缩) |
| 特殊 | returnAddress(jsr指令用,已废弃,使用异常表替代) | 1 Slot | 存储字节码指令地址 |
从下图的demo程序可以看出上述2和3两个特点;静态方法由于没有this引用,比实例方法少一个变量槽;由于变量c在单独的代码块内,变量d会复用变量c的变量槽,导致最大变量槽数比实际定义的变量数少1。


操作数栈
用于存储方法执行过程中的操作数和计算结果,具有以下特点:
1、是一个后入先出(LIFO)栈;和局部变量表一样,操作数栈最大大小在编译期就已经确定,存储在Code属性的max_stacks中;同时操作数栈的每一个元素都可以存储任意的java数据类型,32位数据类型占用的栈容量为1,64位数据类型占用的栈容量为2;
2、通过将字节码指令通过压栈(push)和出栈(pop)操作来进行计算;
3、在方法调用时,调用者通过操作数栈将参数传递给被调用者;
4、在概念模型中,不同栈帧的操作数栈之间数据独立,但是绝大多数虚拟机实现中会进行优化处理,会令两个栈帧出现一部分重叠;栈帧重叠是JVM的一个优化机制,将调用者栈帧的操作数栈栈顶部分和被调用者栈帧的局部变量表底部可以共享一块内存区域,可以减少内存占用、减少数据复制和提高缓存局部性;需要调用者和被调用者的参数布局一致,参数类型和数量在编译期可知,栈帧地址按机器字长对齐;
如下图所示,iload命令将变量压入栈,iadd命令将栈顶两个数弹出并相加,istore命令将计算结果存入变量槽3;下方在调用方法之前,也会先将对应参数压入操作数栈;相关命令意思可通过工具点击"显示JVM规范",查询详细介绍;


最低0.47元/天 解锁文章
1554

被折叠的 条评论
为什么被折叠?



