5.3.8 栈帧

Posted mongotea

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了5.3.8 栈帧相关的知识,希望对你有一定的参考价值。

栈帧由三部分组成:局部变量区,操作数找和帧数据区。局部变量区和操作数栈的大小要 视对应的方法而定,它们是按字长计算的。编译器在编译时就确定了这些值并放在class文件中。 而帧数据区的大小依赖于具体的实现。
当虚拟机调用一个Java方法时,它从对应类的类型信息中得到此方法的局部变量区和操作数 栈的大小,并据此分配栈帧内存,然后压人Java栈中。

局部变量区
Java栈帧的局部变量区被组织为一个以字长为单位、从0开始计数的数组。字 节码指令通过从0开始的索引来使用其中的数据。类型为int、float, reference和returnAddress的值在数组中只占据一项,而类型为byte、short和char的值在存人数组前都将被转换为int值,因而同样占据一项。但是类型为long和double的值在数组中却占据连续的两项。

在访问局部变量中的long和double值的时候,指令只需指出连续两项中第一项的索引值。例 如某个long值占据第3、4项,那么指令会取索引为3的long值。局部变量区的所有值都是字对齐 的,long和double这样占据两项数组元素的值同样可以起始于任何索引。

局部变量区包含对应方法的参数和局部变量。编译器首先按声明的顺序把这些参数放入局 部变量数组。图5-9描绘了下面两个方法的局部变量区。


我们注意到,在源代码中的byte、short. char和boolean在局部变量区都被转换成了int,在 操作数栈中也一样。前面我们曾经说过,虚拟机并不直接支持boolean类型,因此Java编译器总是用int来表示boolean。但Java虚拟机对byte、short和char是直接支持的,这些类型的值可以作为 实例变置或者数组元素存储在局部变量区,也可以作为类变量存储在方法区中。但在局部变量 区和操作数栈中都会被转换成int类型的值。它们在栈帧中的时候都是当做int来进行处理的,只有当它被存回堆或方法区时,才会转换回原来的类型。

同样需要注意的是作为runClassMethod ()的引用被传递的参数Object o。在Java中,所有 的对象都按引用传递.并且都存储在堆中,永远都不会在局部变量区或操作数栈中发现对象的 拷贝,只会有对象引用。

除了Java方法的参数{编译器首先严格按照它们的声明顺序放到局部变量数组中,而对于真 正的局部变量,它可以任意决定放置顺序,甚至可以用一个索引指代两个局部变量一一比如当 两个局部变量的作用域不重叠时,像下面Example3b中的局部变量i和j就是这种情形。在方法的 前半段.在j开始生效之前,0号索引的入口可以被用来代表i。在方法的后半段,i已经超过了有 效作用域,0号人口就可以用来表示j了。

和其他运行时内存区一样,虚拟机的实现者可以为局部变量区设计任意的数据结构。比如 对于怎样把long和double类型的值存储到两个数组项中,Java虚拟机规范没有指定。假如某个虚 拟机实现的字长为64位,这时就可以把整个long或double数据放在数组中相邻两数组项的低项内, 而使高项保持为空。

操作数栈和局部变量区一样,操作数桟也是被组织成一个以字长为单位的数组。但是和前 者不同的是,它不是通过索引来访问,而是通过标准的栈操作——压找和出栈一来访问的。 比如,如果某个指令把一个值压人到操作数栈中,稍后另一个指令就可以弹出这个值来使用。

虚拟机在操作数找中存储数据的方式和在局部变童区中是一样的,如int、long、 float、 double、reference和returnType的存储。对于byte、short以及char类型的值在压入到操作数栈之前 也会被转换为int。

不同于程序计数器,Java虚拟机没有寄存器,程序计数器也无法被程序指令直接访问。java 虚拟机的指令是从操作数栈中而不是从寄存器中取得操作数的,因此它的运行方式是基于栈的 而不是基于寄存器的。虽然指令也可以从其他地方取得操作数,比如从字节码流中跟随在操作 码(代表指令的字节)之后的字节中或从常量池中,但是主要还是从操作数栈中获得操作数。

虚拟机把操作数栈作为它的工作区——大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈。比如iadd指令就要从操作数栈中弹出两个整数,执行加法运算,其结果 又压回到操作数栈中。看看下面的示例,它演示了虚拟机是如何把两个int类型的局部变量相加 再把结果保存到第三个局部变量的:


iload_0 // push the int in local variable 0
iload_l // push the int in local variable 1
iadd // pop two ints, add then, push result
istore_2 // pop int, store into local variable 2

在这个字节码序列里,前两个指令iload_0和iload_l将存储在局部变量区中索引为0和1的整 数压入操作数栈中,其后iadd指令从操作数栈中弹出那两个整数相加,再将结果压人操作数栈。 第四条指令istore_2则从操作数栈中弹出结果,并把它存储到局部变量区索引为2的位置。


帧数据区除了局部变量区和操作数栈外,Java栈帧还需要一些数据来支持常量池解析、正 常方法返回以及异常派发机制。这些信息都保存在Java栈帧的帧数据区中。

Java虚拟机中的大多数指令都涉及到常量池人口。有些指令仅仅是从常量池中取出数据然后 压人java栈(这些数据的类型包括int、long, float、double和String );还有些指令使用常暈池 的数据来指示要实例化的类或数组、要访问的宇段,或要调用的方法;还有些指令需要常量池 中的数据才能确定某个对象是否属于某个类或实现了某个接口。

每当虚拟机要执行某个需要用到常量池数据的指令时,它都会通过帧数据区中指向常量池 的指针来访问它。以前讲过,常量池中对类型、字段和方法的引用在开始时都是符号。当虚拟 机在常量池中搜索的时候,如果遇到指向类、接口、字段或者方法的人口,假若它们仍然是符 号,虚拟机那时候才会(也必须)进行解析。

除了用于常量池的解析外,帧数据区还要帮助虚拟机处理Java方法的正常结束或异常中止。 如果是通过return正常结束,虚拟机必须恢复发起调用的方法的栈帧,包括设置PC寄存器指向发 起调用的方法中的指令——即紧跟着调用了完成方法的指令的下-个指令。假如方法有返回值, 虚拟机必须将它压入到发起调用的方法的操作数栈。


为了处理Java方法执行期间的异常退出情况,帧数据区还必须保存一个对此方法异常表的引用。异常表会在第17章深入描述,它定义了在这个方法的字节码中受catch子句保护的范围,异 常表中的每一项都有一个被catch子句保护的代码的起始和结束位置(译者注:即try子句内部的 代码),可能被catch的异常类在常量池中的索引值,以及catch子句内的代码开始的位置。

当某个方法抛出异常时,虚拟机根据帧数据区对应的异常表来决定如何处理。如果在异常 表中找到了匹配的catch子句,就会把控制权转交给catch子句内的代码。如果没有发现,方法会 立即异常中止。然后虚拟机使用帧数据区的信息恢复发起调用的方法的帧,然后在发起调用的 方法的上下文中重新抛出同样的异常。

除了上述信息(支持常量池解析、正常方法返回和异常派发的数据〉外,虚拟机的实现者 也可以将其他信息放入帧数据区,如用于调试的数据等。

Java栈的可能实现方式实现的设计者可以任意按自己的想法设计Java栈,正如前面提到的,一个可能的方式就是从堆中分配每一个帧。例如,考虑下面的类:


注意addAndPrint ()方法也要使用常量池才能确定addTwoTypes ()方法——尽管这两个 方法是属于一个类的。由此可见,和引用其他类的字段或方法一样,对同一个类的方法和字段 的引用初始时也是符号,因此在使用之前同样需要解析。

解析后的常量池数据项将指向方法区中对应方法addTwoTypes ()的信息。虚拟机需要使用 这些信息来决定addTwoTypes ()的局部变量区和操作数栈的大小。如果使用Sun的javac编译器


(JDK 1.1 )的话,addTwoTypes ()的局部变量区需要3个字长,操作数栈需要4个字长(帧数据区的大小依赖于具体的实现)。虚拟机紧接着从堆中为这个方法分配足够大的栈帧。然后从方法 addAndPrint ()的操作数栈中弹出double参数和int参数( 88.88和1 ),并把它们分别放在 addTwoTypes ()的局部变量区中索引为1和0的位置。

当addTwoTypes ()返回时,它首先把类型为double的返回值(这里是89.88 )压人自己的操 作数栈里。紧接着虚拟机使用帧数据区中的信息找到调用者(即addAndPrint ())的栈帧,然后 将返回值压人addAndPrint ()的操作数栈中并释放方法addTwoTypes ()的栈帧所占用的内存。最后虚拟机把addTwoTypes ()的栈帧作为当前帧,从调用执行的下一条指令开始继续执行方法addAndPrinl()

图5_12显示了另一种虚拟机实现执行同一方法的java栈快照。它的栈帧不是从堆中单独分配, 而是从一个连续的栈中分配,因而这种方式允许相邻方法的栈帧可以相互重叠。这里调用者的 操作数栈部分(它包含要传给被调用者的参数)就成了被调者的局部变量区的底层。在这个例 子中,addAndPrint ()的整个操作数栈刚好成为addTwoType ()的整个局部变量区。


这种方式不仅节省了内存空间,因为发起调用的方法和被调用的方法用相同的内存保存参 数,而且也节省了时间,因为虚拟机不再需要费时地把参数从一个栈帧拷贝到另一个栈帧中了。

注意当前帧的操作数栈总是在Java栈的“顶部“。尽管这样可能可以更好地说明图5-12中连续内存的实现,但不管Java栈是如何实现的(前面说过,在本书中所有涉及栈的图形中,栈是从上向下生长的。栈的’顶部‘一直在图形中处于底部),对操作数栈的压人(或者从栈中弹出)总是在当前帧执行的。这样,在当前帧的操作数栈中压人一个值也可以看做是往整个Java栈压人 一个值。在本书的剩余部分,我们说”把一个值压入栈”都是指把值压入当前帧的操作数栈。 Java栈还有-些其他的实现方式,但基本上都是图5-11和图5-12两种情形的混合。比如虚拟机可以在线程启动时就从栈中分出一大段空间,之后只要还在这段连续的空间里,虚拟机都可 以采用如图5-12所示的重叠方法。如果栈生长超过了这段连续空间,虚拟机可以再从堆中分配 另一段空间。如果发起调用的方法的栈帧位于旧的那段空间中,而被调用的方法的栈帧位于新 的那段空间中,就使用如图5-11所示的方法把它们连接起来。在新的空间段中,虚拟机又可以继 续使用连续内存方法。

 

以上是关于5.3.8 栈帧的主要内容,如果未能解决你的问题,请参考以下文章

5.3.8 栈帧

函数调用过程原理及栈帧分析

JVM之一:操作数栈和局部变量表

JVM之一:操作数栈和局部变量表

JVM之一:操作数栈和局部变量表

Java虚拟机栈和内存模型