4.3.1. 关于 Slot 的理解
局部变量表,最基本的存储单元是 Slot(变量槽)
参数值的存放总是在局部变量数组的 index 0 开始,到数组长度-1 的索引结束。
局部变量表中存放编译期可知的各种基本数据类型(8 种),引用类型(reference),returnAddress 类型的变量。
在局部变量表里,32 位以内的类型只占用一个 slot(包括 returnAddress 类型),64 位的类型(long 和 double)占用两个 slot。
byte、short、char 在存储前被转换为 int,boolean 也被转换为 int,0 表示 false,非 0 表示 true。
long和double则占据两个Slot
JVM 会为局部变量表中的每一个 Slot 都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个 slot 上
如果需要访问局部变量表中一个 64bit 的局部变量值时,只需要使用前一个索引即可。(比如:访问 long 或 double 类型变量)
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用 this 将会存放在 index 为 0 的 slot 处,其余的参数按照参数表顺序继续排列。
方法与局部变量表对照图:
4.3.2. Slot 的重复利用
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
Slot重复利用示意图:
4.3.3. 静态变量与局部变量的对比
参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。
我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。
和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。
public void test(){ int i; System. out. println(i);//System.out.println(num);//错误信息:Variable 'num' might not have been initialized }
这样的代码是错误的,没有赋值不能够使用。
**补充:**变量的分类:
按照数据类型分:① 基本数据类型 ② 引用数据类型
按照在类中声明的位置分:
成员变量:在使用前,都经历过默认初始化赋值.
类变量(静态变量): linking的prepare阶段:给类变量默认赋值 —> initial阶段:给类变量显式赋值即静态代码块赋值
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过
4.3.4. 补充说明
在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
4.4. 操作数栈(Operand Stack)
每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈
比如:执行复制、交换、求和等操作
代码举例
public void testAddOperation(){ byte i = 15; int j = 8; int k = i + j; }
字节码指令信息
public void testAddOperation(); Code: 0: bipush 15 2: istore_1 3: bipush 8 5: istore_2 6:iload_1 7:iload_2 8:iadd 9:istore_3 10:return
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是 JVM 执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的 Code 属性中,为 max_stack 的值。
栈中的任何一个元素都是可以任意的 Java 数据类型
32bit 的类型占用一个栈单位深度
64bit 的类型占用两个栈单位深度
操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新 PC 寄存器中下一条需要执行的字节码指令。
操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
另外,我们说 Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
**备注:**操作数栈和局部变量表的底层都是数组,所以对于double和long类型数据需要两个单位存储.
4.5. 代码追踪
public void testAddOperation() { byte i = 15; int j = 8; int k = i + j; }
使用 javap 命令反编译 class 文件: javap -v 类名.class
public void testAddoperation(); Code: 0 bipush 15 2 istore_1 3 bipush 8 5 istore_2 6 iload_1 7 iload_2 8 iadd 9 istore_3 10 return
涉及操作数栈的字节码指令执行分析:
- istore 指令会导致出栈 并且写入局部变量表 ipush:放入操作数栈
- istore和istore一样,只不过istore只有0到3(其实是四个不同的指令,操作数隐式指定),再往后就得用istore了,因为还需要显式指出槽位,所以要占两个字节.