Java虚拟机知识整理——运行时栈帧结构

本文介绍了Java虚拟机栈帧的结构,包括运行时栈帧、局部变量表、操作数栈、动态连接、方法返回地址及附加信息。栈帧是虚拟机执行方法的基础,它存储了方法的局部变量、操作数、动态连接信息等。局部变量表以变量槽为单位,用于存储参数和局部变量。操作数栈遵循后入先出原则,用于方法执行过程中的数据运算。动态连接指向方法的运行时常量池引用。方法返回地址在正常或异常退出时用于恢复调用者的状态。
摘要由CSDN通过智能技术生成

运行时栈帧

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部表量表、操作数栈,动态连接和方法返回地址等信息。每一个方法从调用开始执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动县城中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关的方法称为当前方法。执行引擎运行的所有字节码指令都值针对当前栈帧进行操作。

局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内不定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性max_locals数据项中确定了该方法所需要的分配局部变量表的最大容量。
局部表量表的容量以变量槽为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性地说道每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放,但这种描述与明确指出“每个Slot占用32位长度的内存空间”是有一些差别的,它允许Slot的长度可以随着处理器、操作系统或虚拟机的不同而发生变化。
虚拟机的八种数据类型,前六种是Java的数据类型,第七种reference类型表示对一个对象实例的引用,虚拟机规范既没有说明它的长度,也没有明确指出这种引用应有怎样的结构。但一般来说,虚拟机实现至少都应当知道这个引用做到两点,意识从此引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,否则无法实现Java语言规范中定义的语法结构约束。第八种returnAddress类型目前已经很少见了,它是为字节码指令jsr、jsr_w和ret服务的,指向了一条字节码指令的地址,很古老的Java虚拟机曾经使用这几条指令来实现异常处理,现在已经由已经由异常表代替。
对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。
虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部表量表最大的Slot数量。如果访问的是32为数据类型的变量,索引n就代表了使用第n个Slot,如果是64位数据类型的变量,则说明会同时使用n和n+1两个Slot。对于两个相邻的共同存放一个64位数据的两个Slot,不允许采用任何方式单独访问其中的某一个,Java虚拟机规范中明确要求了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。
在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量表到参数变量列表的传递过程的,如果执行的是实例方法,那局部表量表中第0为所以的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含的参数。崎岖参数则按照参数表排列,占用从1开始的局部标量slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。
为了尽可能节省帧栈空间,局部变量表中的Slot是可以宠用的,方法体中定义的变量,器作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超过了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。不过这样的设计除了节省栈帧的空间之外,还会伴随一些额外的副作用,例如,在某些情况下,Slot的复用会直接影响到系统的垃圾收集行为。

操作数栈

操作数栈,也常成为操作栈,它是一个后入现出栈。同局部变量一样,操作数栈的最大深度也在编译的时候写到了Code属性的max_stacks数据项中。操作书栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占用的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值。
当一个方法刚刚开始执行的时候,这个方法低的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是入栈/出栈操作。
操作数栈中元素的数据类型不许与字节码指令的序列严格匹配,在贬义程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再验证这一点。

动态连接

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

方法返回地址

每当一个方法开始执行后,只有两种方式可以退出这个方法。
第一种是执行引擎遇到的任意一个返回的字节码指令,这个时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。
另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无聊时Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
无论采用何种退出方式,在方法退出之后,就需要放回到方法调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存就一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出是,调用者的PC计数器的值可以作为返回地址,栈帧中和可能会保存这个计数器值,而方法异常退出时,返回地址是要通过异常持利器表来确定的,栈帧中一般不会保存着部分信息。
方法的退出过程实际上就等同于吧当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以执行方法调用指令后面的一条指令等。

附加信息

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值