前言
执行引擎是Java虚拟机最核心的组成部分之一。「虚拟机」是相对于「物理机」的概念,这两种机器都有代码执行的能力,区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机执行引擎是在此基础上实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。
概念
JVM的字节码执行引擎:输入的是字节码文件,然后对字节码进行解析并处理,最后输出执行的结果。
在Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观(Facade)。在不同的虚拟机实现里,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种方式,也可能两者都有,甚至还可能会包含几个不同级别的编译器执行引擎。但从外观上来看,所有Java虚拟机的执行引擎是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。
一、运行时栈帧结构
栈帧(StackFrame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量、操作数栈、动态链接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
每一个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
一个线程中的方法调用链可能会很长,很多方法都处于执行状态。对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(CurrentStackFrame),与这个栈帧相关联的方法称为当前方法。执行引擎运行的所有字节码指令对当前栈帧进行操作,在概念模型上,典型的栈帧结构如下图:
局部变量表
局部变量表(LocalVariableTable)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序中编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
1、以变量槽slot为单位,目前一个slot存放32位以内的数据类型。
2、对于64位的数据占2个slot.
3、对于实例方法,第0位slot存放的是this,然后从1到n,依次分配给参数列表。
4、然后根据方法体内部定义的变量顺序和作用域来分配slot。
5、slot是复用的,以节省栈帧的空间,这种设计可能会影响到系统的垃圾收集行为。
操作数栈
操作数栈(OperandStack)是一个后进先出栈。同局部变量表一样,操作数栈的最大深度也在编译阶段写入到Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过max_stacks数据项中设定的最大值。
一个方法刚开始执行的时候,该方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是入栈和出栈操作。
操作数栈:用来存放方法运行期间,各个指令操作的数据。
1、操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配
2、虚拟机在实现栈帧的时候可能会做一些优化,让两个栈帧出现部分重叠的区域,已存放公用的数据。
动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接(DynamicLinking)。Class文件的常量池中存在大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化成为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
方法返回地址
方法返回