执行引擎是Java虚拟机核心的组成部分之一。
“虚拟机”是一个相对于“物理机”的概念,这两种机 器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层 面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执 行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
在不同的虚拟机实现中,执行引擎在执行字节码的 时候,通常会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种 选择,也可能两者兼备.
从外观上来看,所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处 理过程是字节码解析执行的等效过程,输出的是执行结果
运行时栈帧结构
Java虚拟机以方法作为最基本的执行单元,“栈帧”(Stack Frame)则是用于支持虚拟机进行方法 调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
每 一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。 在编译Java程序源码的时候,栈帧中需要多大的局部变量表,需要多深的操作数栈就已经被分析计算 出来,并且写入到方法表的Code属性之中。换言之,一个栈帧需要分配多少内存,并不会受到程序 运行期变量数据的影响,而仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式。
以Java程序的角度来看,同一时刻、同一条线程里面,在 调用堆栈的所有方法都同时处于执行状态。而对于执行引擎来讲,在活动线程中,只有位于栈顶的方 法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”(Current Stack Frame),与 这个栈帧所关联的方法被称为“当前方法”(Current Method)。执行引擎所运行的所有字节码指令都只 针对当前栈帧进行操作.
接下来,我们将会详细了解栈帧中的局部变量表、 操作数栈、动态连接、方法返回地址等各个部分的作用和数据结构。虚拟机栈和栈帧的总体结构:
局部变量表
局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义 的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方 法所需分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot)为最小单位,《Java虚拟机规范》中并没有明确指出 一个变量槽应占用的内存空间大小,只是很有导向性地说到每个变量槽都应该能存放一个boolean、 byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型,都可以使用32位 或更小的物理内存来存储。
一个变量槽可以存放一个 32位以内的数据类型,Java中占用不超过32位存储空间的数据类型有boolean、byte、char、short、int、 float、reference和returnAddress这8种类型。
reference类型表示对一个对象实例的引用,但是一般来说,虚拟机实现至少都应当能通过这个引 用做到两件事情:
一是从根据引用直接或间接地查找到对象在Java堆中的数据存放的起始地址或索 引
二是根据引用直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息。
第8种returnAddress类型目前已经很少见了。
对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。Java语言 中明确的64位的数据类型只有long和double两种。这里把long和double数据类型分割存储的做法与“long 和double的非原子性协定”中允许把一次long和double数据类型读写分割为两次32位读写的做法有些类 似。由于局部变量表是建立在线 程堆栈中的,属于线程私有的数据,无论读写两个连续的变量槽是否为原子操作,都不会引起数据竞 争和线程安全问题。
Java虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的变 量槽数量。如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽,如果访问的是64位 数据类型的变量,则说明会同时使用第N和N+1两个变量槽。
当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程, 即实参到形参的传递。如果执行的是实例方法(没有被static修饰的方法),那局部变量表中第0位索 引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐 含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量槽,参数表分配完毕后,再根据 方法体内部定义的变量顺序和作用域分配其余的变量槽。
关于局部变量表,还有一点可能会对实际开发产生影响,就是局部变量不像前面介绍的类变量那 样存在“准备阶段”。我们已经知道类的字段变量有两次赋初始值的过程,一次在准 备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。因此即使在初始化阶 段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值,不会产生歧义。但局部 变量就不一样了,如果一个局部变量定义了但没有赋初始值,那它是完全不能使用的。