16、JVM实战:运行时栈帧结构

16–运行时栈帧结构


1、栈帧组成部分

1. 局部变量表
2. 操作数栈
3. 动态连接
4. 方法返回地址和一些额外的附加信息。

在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。

一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引用所运行的所有字节码指令都只针对当前栈帧进行操作。

2、栈帧的概念结构

*

3、局部变量表

1、 是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量;
2、 在编译Class文件时,就在方法的Code属性的max_locals数据项中已经确定了该方法需要分配的局部变量表的最大容量;

3.1、Slot(Variable Slot)变量槽

1、 是局部变量表的最小单位,没有强制规定大小为32位,虽然32位足够存放大部分类型的数据;
2、 一个Slot可以存放boolean、byte、char、short、int、float、reference和returnAddress8种类型;

1、 reference表示对一个对象实例的引用,通过它可以得到对象在Java堆中存放的起始地址的索引和该数据所属数据类型在方法区的类型信息;
2、 returnAddress则指向了一条字节码指令的地址;
3、 对于64位的long和double变量而言,虚拟机会为其分配两个连续的Slot空间;

3.2、局部变量表的索引分配

1、 jvm通过索引定位的方式使用局部变量表;
2、 局部变量表存放的是方法参数和局部变量,当调用方法是非static方法时,局部变量表中第0位索引的Slot存储的是"this"关键字指向的对象分配完方法参数后,便会依次分配方法内部定义的局部变量;

3.3、Slot复用验证

1、 为了节省栈帧空间,局部变量表中的Slot是可以重用的如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用;
2、 这种机制有时候会影响垃圾回收行为;

3.3.1、测试1

public class Test {
        public static void main(String[] args) {

            byte[] p = new byte[64*1024*1024];
            System.gc();
        }
}

[GC (System.gc())* 71442K->66576K(376832K), 0.0224084 secs]
[Full GC (System.gc())* 66576K->66462K(376832K), 0.0066806 secs]

没有回收p所占的内存,因为在执行System.gc()时,变量p还处于作用域内,虚拟机自然不敢回收p的内存。

3.3.2、测试2

*

p的所用域被限制在了块中,执行System.gc()的时候。p已经不能被访问了。可还是没有被回收。

3.3.3、测试3

*

垃圾被回收了。

3.3.4、总结

1、 jvm不会给局部变量赋初始值,只给全局变量赋初始值;
2、 slot复用会打断slot中变量与对象的引用,这种关联被打断后,垃圾回收才会生效;
3、 虽然块中的变量不可能在块外被访问,妥妥的垃圾了,但是slot和对象的关联仍然保持,不会成为垃圾;

*

4、操作数栈

1、 常称为操作栈;
2、 后入先出栈;
3、 Class文件的Code属性的max_stacks指定了执行过程中最大的栈深度;
4、 Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,这里的栈就是指操作数栈;
5、 是java虚拟机栈中一个用于计算的临时数据存储区;
6、 运算的地方,大多数指令都在操作数栈弹栈运算,然后结果压栈;

4.1、案例:演示了虚拟机是如何把两个int类型的局部变量相加,再把结果保存到第三个局部变量

iload_0****//*存储在局部变量中索引为0的整数压入操作数栈中
iload_1****//**存储在局部变量中索引为1的整数压入操作数栈中
iadd*******// 从操作数栈中弹出那两个整数相加,再将结果压入操作数栈
istore_2***//*操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置

4.2、原理图

*

5、动态链接

1、 每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(DynamicLinking);
2、 Class文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接;

6、方法返回地址

6.1、当一个方法开始执行以后,只有两种方法可以退出当前方法

1、 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(NormalMethodInvocationCompletion),一般来说,调用者的PC计数器可以作为返回地址;
2、 当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(AbruptMethodInvocationCompletion),返回地址要通过异常处理器表来确定;

6.2、当方法返回时,可能进行3个操作:

1、 恢复上层方法的局部变量表和操作数栈;
2、 把返回值压入调用者调用者栈帧的操作数栈;
3、 调整PC计数器的值以指向方法调用指令后面的一条指令;

7、附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。
在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。