第二章(三) Java虚拟机结构-栈帧

2.6 栈帧
    栈帧用来存储变量值、中间结果,也用来进行动态链接、返回访法值和分发异常。
    栈帧在一个方法调用时创建,在方法调用完成后销毁,不管方法是正常结束,还是非正常结束(方法抛出异常)。栈帧从创建此栈帧的线程对应的虚拟机栈中分配存储空间。每个栈帧有自己的本地变量表、操作数栈以及一个指向当前方法对应类的运行时常量池的引用。
    本地变量表和操作数栈的大小在编译时确定,随着当前栈帧对应方法的代码一起提供。因此栈帧数据结构的大小只和虚拟机的实现相关,这些数据结构所需的内存可以在方法调用同时分配。
    在一个线程中,任意时刻只有正在执行着的方法对应栈帧是活跃的。这个栈帧称为当前栈帧,对应的方法称为当前方法。当前方法所在的类称为当前类。本地变量表和操作数栈对应着当前的栈帧。
    当一个方法调用另一个方法或者结束时对应的栈帧不再是当前栈帧。当一个方法被调用,控制转到新方法是,一个新的栈帧被创建,并变成当前栈帧。当方法返回时,如果当前栈帧有返回值,就会将值传递给之前栈帧,然后随着之前的栈帧变成当前栈帧此栈帧就会销毁。
    一个线程创建的栈帧对本线程是私有的,任何其他的线程都不能访问
2.6.1 本地变量表
    每个栈帧包含一个称为本地变量表的变量数组,变量数组的长度在编译时刻就已经确定,在一个类或接口的二进制表示中提供,随着一个栈帧对应的方法调用的编码一起提供。
    单个本地变量中能存储boolean, byte, char, short, int,float,reference,returnAddress等类型的值,一对本地变量能存储long和double类型对应的一个值。
    本地变量是用索引来寻址的。本地变量的第一个索引值是0。只有一个在0和变量数组长度范围内的整数值才是一个正确的索引值。
    一个long和double类型的值占据连续的两个变量值。这个值只能从小的索引值访问。例如,一个double值存储在索引值为n的本地变量数组中,实际上占领n和n+1的本地变量表中,但是索引值为n+1的值是不能读取出来,只能写入,如果n+1对应的变量表中被重新写入,n对应的内容就没有实际意义。
    Java虚拟机不需要存储long和double值对应的n必须是偶数。直观而言,long和double对应的值在本地变量表中并不必须是64位的。实现者可以自由决定用合适的两个变量来存储这类值。
    Java虚拟机在方法调用时用本地变量表传递参数,当类方法调用时,变量从0开始按顺序连续存储到对应的本地变量表中。当实例方法调用时,本地变量0的位置总是用来传递被调用方法所在类的一个引用(即Java语言中的this),所有的变量按顺序连续存储在从1开始的本地变量表中。
2.6.2 操作数栈
    每个栈帧包含一个被称为操作数栈的后进先出栈(LIFO),每个栈帧的操作数栈的深度在编译时刻就已经确定,随着一个栈帧对应的方法调用的编码一起提供。
    如果上下文比较清楚,下面有的地方会直接用操作数栈代表当前栈帧对应的操作数栈。
    当一个栈帧创建时对应的操作数栈是空的。Java虚拟机提供指令集把常量或数值从本地变量表或字段中加载到操作数栈中。还有一些指令从操作数栈中取出操作数,对它们进行运算,并把结果放回到操作数栈中。操作数栈也被用来准备传递参数给方法和接收方法的返回结果。例如:iadd指令把两个int类型的数值相加,这个指令需要事先有指令将被操作的两个整数值放到操作数栈的最上面,这两个整数值从操作数栈中取出来,相加,再把相加的结果放到操作数栈的最上面。操作数栈中可以嵌套子运算,其结果再被包含它的运算使用。
    操作数栈中的每个条目都可以装载任意一个Java虚拟机类型的值,包括long和double类型的值。
    操作数栈中的值必须按照其类型进行相应的操作。例如,你不能向操作数栈中压入两个整数,后面把它们当做long类型来操作,也不能够压入两个float类型的值,后面有iadd指令来操作。一小部分虚拟机指令如dup、swap将运行时数据区作为原始值操作而不用考虑对应的数据类型,这些指令以如此的方式定义,所以它们不能用来修改、打破单独的数值。这些在操作数栈上的一些操作限制是通过class文件来验证确保的。在任意的时点,操作数栈都有一个相应的深度,long和double类型占有两个单元,其他数据类型占用一个单元。
    2.6.3 动态链接
    每一个栈帧都包含一个对当前方法所在类型的运行时常量池的引用,用来支持方法代码的动态链接。在类文件中,一个方法对另外的方法调用、对变量的访问都是通过符号引用。动态链接将这些方法的符号引用转换成具体的方法调用,当需要时加载需要的类类解决到当前仍未确定的符号,将对变量的访问转换成相对应的存储架构中的偏移值,用来和这些变量在运行时存储位置相关联。
    这种后期绑定方法和变量的方式,使得当方法引用的其他类变化时尽少可能的改变当前类的编码。
    2.6.4 方法调用正常结束
    如果一个方法调用没有抛出异常:既没有虚拟机抛出异常,也没有显式的调用throw语句,那么这个方法就是正常调用结束。如果一个方法正常结束,那么它有可能给调用它的方法返回一个值。这种情况是当被调用的方法执行一个return指令,并且return指令必须要符合方法声明的类型。
    当前的栈帧这时就会加载调用者的状态,包括本地变量表、操作数栈和程序计数器,此时程序计数器会适当的增加来跳过刚执行的方法调用指令,并把刚调用的方法的返回值(如果有)压入操作数栈中,在调用者的栈中继续正常的执行。
    2.6.5 方法调用异常结束
    如果在方法中执行Java虚拟机指令引起虚拟机抛出一个异常,并且这个异常没有被方法捕获,那么这个方法就会异常结束。另外,执行athrow指令,也会显式的抛出一个异常,如果异常没有被方法捕获处理,也将引起方法的异常结束。一个异常结束的方法从不会给它的调用者返回值。

猜你喜欢

转载自fengyilin.iteye.com/blog/2269939