当前位置: 首页 > 后端技术 > Java

一篇快速了解Java虚拟机栈帧结构的文章

时间:2023-04-01 13:17:01 Java

什么是栈帧?众所周知,Java虚拟机的内存区域分为程序计数器、虚拟机栈、本地方法栈、堆、方法区。(什么?你还不知道,快去看看《Java虚拟机内存结构及编码实战》)这次要介绍的StackFrame是Java虚拟机中VirtualMachineStack的基本元素。它还用于支持Java虚拟机的方法调用和方法执行背后的数据结构。了解它可以更好地理解Java虚拟机执行引擎是如何工作的。每个方法从开始调用到执行结束的整个过程对应于一个栈帧在虚拟机栈中从入栈到出栈的过程。栈帧中存放了方法的局部变量表、操作数栈、动态连接、方法返回地址等信息。同时,在同一个线程中,只有栈顶的方法在运行,只有栈顶的栈帧有效,执行引擎运行的所有字节码指令都只在栈顶运行当前堆栈帧。虚拟机栈和栈帧的整体结构如下:下面分别介绍栈帧中的局部变量表、操作数栈、动态链接、方法返回地址的功能和数据结构。局部变量表(LocalVariablesTable)局部变量表是用来存放一组变量值的内存空间,用来存放方法参数和方法内部定义的局部变量。在编译后的Class文件中,方法的Code属性的max_locals数据项决定了该方法需要分配的局部变量表的最大容量。局部变量表的容量以变量槽(VariableSlot)为最小单位,每个变量槽存储一个32位的数据类型,如boolean、byte、char、short、int、float、reference。前6种同学应该都懂,就不用介绍了。引用类型表示对对象实例的引用。通过这个引用可以做两件事:根据引用直接或间接地在Java堆中找到实例的数据存储的起始位置或索引;根据引用直接或间接查找存储在方法区的类信息。对于64位的数据类型,比如long、double,两个连续的变量槽空间按照高位对齐的方式进行分配。使用局部变量表时,通过索引定位到相应数据的位置,索引值取值范围为0到局部变量表中最大变量槽数。如果访问一个32位数据类型的变量,索引N表示使用第N个变量槽,如果访问一个64位数据类型的变量,则表示将使用N和N+1两个变量槽同时使用。对于存储一个64位数据的两个相邻的变量槽,虚拟机不允许以任何方式独立访问其中一个。如果遇到了这个操作的字节码,Java虚拟机就会在类加载的验证阶段抛出一个异常。当一个方法被调用时,通过局部变量表来完成参数值到参数变量列表的传递过程。如果你执行的是对象实例的成员方法(没有被static修饰的方法),那么局部变量表中索引为0的变量槽默认是对象实例的引用,可以通过在方法隐式参数中使用关键字this。其余参数按照参数列表的顺序排列。参数列表分配完毕后,其余变量槽按照方法体内定义的局部变量的顺序和作用域进行分配。为了尽可能节省栈帧消耗的内存空间,可以复用局部变量表中的变量槽。当方法体中定义的局部变量超出其作用域时,可以将局部变量对应的变量槽交给其他变量重用。前面《JVM的类加载机制全面解析》讲过,在类加载过程中,类变量有两个初值赋值过程,一个在准备阶段,给系统赋初值;另一次在初始化阶段,分配代码中定义的初始值。因此,即使没有给类变量赋值,也没关系,类变量还是有确定的初值的,不会出现歧义。但是局部变量没有类变量那样的“准备阶段”。如果定义了局部变量但未分配初始值,则根本无法使用它。所以不要以为在Java中有任何情况下整数变量默认为0,布尔变量默认为false等默认值规则。例如:publicclassOneMoreStudy{publicstaticvoidmain(String[]args){inti;System.out.println(i);}}因为局部变量i没有初始化,编译过程中会报错:Error:(4,28)java:Thevariableimaynothavebeeninitialized。操作数栈(OperandStack)是一个后进先出(LIFO)栈。和局部变量表一样,在编译后的Class文件中,方法的Code属性的max_stacks数据项决定了方法分配的操作数栈的最大深度。在方法执行期间的任何时候,操作数栈的深度都不会超过max_stacks数据项中设置的最大值。操作数栈的每个元素都可以是任何Java数据类型,包括long和double。32位数据类型占用的栈容量为1,64位数据类型占用的栈容量为2。当一个方法刚开始执行时,该方法的操作数栈为空。在方法执行过程中,各种字节码指令会弹出和压入操作数栈。例如,整数加法的字节码指令iadd必须保证在指令执行前最接近操作数栈顶的两个元素已经存储了两个int值。将两个int值出栈相加,相加结果压回栈中。操作数栈中元素的数据类型必须严格匹配字节码指令的顺序。在编译代码的时候,编译器会严格保证这一点,在类加载的验证阶段也会再次验证这一点。在上面的iadd指令中,它只能用于整数相加。执行时,最靠近栈顶的两个元素的数据类型必须是int类型,不能使用iadd命令添加其他数据类型。.当一个方法调用另一个方法时,可以通过操作数栈传递方法参数。虽然在Java虚拟机规范中,两个不同的栈帧作为不同方法的虚拟机栈的元素是完全相互独立的。但是,在大多数Java虚拟机的实现中,都会进行一些优化:两种不同方法的一些栈帧重叠。让下层栈帧的部分操作数栈与上层栈帧的部分局部变量表重叠。这样不仅节省了一些内存空间,更重要的是在进行方法调用时可以直接共享部分数据。额外的参数被复制和传递,如下图所示:动态链接(DynamicLinking)每个栈帧在运行时常量池中包含对栈帧所属方法的引用。保留此引用以支持方法调用期间的动态。连接。之前的《Class文件结构全面解析》中提到,Class文件的常量池中存在大量的符号引用,这些符号引用中有一部分会在类加载阶段或者第一次使用时转化为直接引用时间(实际运行时内存布局中的入口地址),这种转换称为静态解析。另一部分会在每次运行时转化为直接引用,这部分称为动态链接。关于这两个转换过程的具体过程,这里先说一个小故事,后续文章会详细介绍。方法返回地址方法返回时,可能需要在栈帧中保存一些信息,以恢复调用者(调用当前方法的方法)的执行状态。一般来说,当方法正常退出时,调用者的程序计数器的值可以作为返回地址,而计数器的值很可能保存在栈帧中。当方法异常退出时,返回地址是通过异常处理表来确定的,栈帧中一般不会保存这部分信息。方法返回的过程其实相当于弹出当前栈帧。可能的操作包括:恢复调用者的局部变量表和操作数栈,将返回值(如果有的话)压入调用者的栈帧。在堆栈中,调整程序计数器的值,使其指向方法调用指令之后的指令,依此类推。附加信息在Java虚拟机规范中,允许Java虚拟机在栈帧中添加一些规范中没有描述的信息,例如:与调试和性能收集相关的信息。这部分信息完全取决于具体的虚拟机实现。一般把动态连接、方法返回地址等附加信息都归为一类,称为栈帧信息。总结栈帧是Java虚拟机中虚拟机栈的基本元素。每个方法从调用开始到执行结束的整个过程对应于一个栈帧在虚拟机栈中从入栈到出栈的过程。栈帧中存放了方法的局部变量表、操作数栈、动态链接、方法返回地址等附加信息。局部变量表用于存放方法参数和方法内部定义的局部变量;执行各种字节码指令时,会弹出和压入操作数栈;动态链接是指向运行时常量池中栈帧所属方法的引用;方法返回地址用于恢复调用当前方法的方法的执行状态。