运行时帧栈结构
帧栈
是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。帧栈存储了方法的局部变量表、操作数表、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个帧栈在虚拟机里的出入栈过程。
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈,与这个栈相关的方法被称为当前方法。
局部变量表
是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。最小单位是Slot,虚拟机规范中并没有明确一个Slot应占用的内存大小。指定Boolean、byte、char、short、int、float、reference或returnAddress都可以用一个Slot来存储。long、double(64位)需要两个连续的Slot来存放。在方法执行时,虚拟机使用局部变量表完成参数值到参数列表的传递过程,如果执行的是实例方法(非static方法),那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过 ’ this ‘ 来访问到这个隐含参数。
操作数栈
操作数栈也被称为操作栈,它是一个后入先出(LIFO)栈。同局部变量表一样,操作数栈的最大深度也是在编译的时候写入Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的容量为1,64位所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过max_stacks数据项中的最大值。概念模型中两个帧栈作为虚拟机栈的元素是相互独立的。但在大多数虚拟机的实现里会做一些优化,让下面帧的部分操作数栈与上面帧栈的部分局部变量表重叠在一起,这样在进行方法调用的时候就可以公用一部分数据,无需进行额为的参数复制传递。
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持这个方法调用过程中的动态连接。
方法返回地址
当一个方法开始执行后,只有两种方式可以推出这个方法。第一种方式是执行引擎遇到了任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),这种被称为正常完成出口。另一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理(在本方法内的异常表中没有搜索到匹配的异常处理器),就会导致方法退出,这种退出方法的方式被称为异常完成出口。
方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
基于栈的指令集和基于寄存器的指令集
JAVA编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流中的指令大多数都是零地址指令,它们依赖操作数栈进行工作。与之相对应的另一套常用的指令集架构就是寄存器指令集,最典型的就是x86的二地址指令集,就是现在我们主流PC机中直接支持的指令集架构,这些指令依存寄存器进行工作。
区别:基于栈的指令集主要优点就是可移植性,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。如果使用栈架构的指令集,用户程序不会直接使用这些寄存器,就可以由虚拟机实现来自行决定把一些访问最频繁的数据(程序计数器、栈顶缓存等)放到寄存器中以获得良好的性能,这样的实现简单。还有其他有点,如代码比较紧凑(字节码中的每个字节都对应一条指令,而多地址指令集中还需要存放参数)、编译更加简单(不需要考虑空间分配问题,所需空间都在栈上操作)。缺点就是执行速度相对较慢,这也是目前主流的都是寄存器指令集。代码虽然紧凑但是完成相同功能所需指令数量一半会比寄存器架构多,而且栈的实现在内存中,频繁的出入栈也意味着频繁的内存访问,相对于处理器来说,内存始终是执行速度的瓶颈。
网友评论