Java虚拟机字节码执行引擎是jvm最核心的组成部分之一,它做的事情很简单:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。在不同的虚拟机实现里,执行引擎在执行java代码的时候可能会有解释执行和编译执行两种选择,也可能两者兼备。
运行时栈帧结构java字节码执行引擎在调用和执行方法的时候使用了一种叫做栈帧的数据结构。
在jvm的内存结构里,存在这一块称为虚拟机栈的内存区域,虚拟机栈中的元素就是栈帧。每个方法的调用至结束对应着一个栈帧在虚拟机栈的入栈和出栈。
栈帧数据结构示意图:
如图所示,因为虚拟机栈是线程私有的,所以每个线程都有自己的虚拟机栈;而每个线程的虚拟机栈中都有多个栈帧对应一个方法调用链中的多个方法,栈顶的栈帧是当前正在执行的方法;每个栈帧主要包含4个部分:
局部变量表
操作数栈
动态连接
方法返回地址
在编译代码的阶段,栈帧中需要多大的局部变量表,多深的操作数栈都是已经完全确定的,而且会写入到方法表的Code属性中。
接下来一次介绍栈帧中的4个部分:
局部变量表局部变量表是一组变量值存储空间,用于存放方法参数和方法内局部变量。
局部变量表的容量以变量槽Slot为最小单位,规定一个Slot可以存放一个32位以内的数据类型,java中占32位以内的数据类型有8种基本数据除了long和double以外的6种,还有reference和returnAddress,共8种类型。而对于64位的数据类型,即long和double,则会以高位对齐的方式为其分配两个连续的Slot空间。
在方法执行时,方法的参数列表是通过局部变量表传递的,如果执行的是实例方法(非static方法),则局部变量表中的第0位索引的Slot默认保存方法所属对象的引用,以支持“this”关键字来访问对象数据。其余参数则按参数列表顺序,占用从1开始的局部变量Slot,参数表分配完成后,在根据方法体内局部变量定义的顺序和作用域为局部变量分配其余Slot。之所以这里要强调作用域是因为为了节省栈帧空间,局部变量表中的Slot是可以重用的,当一个Slot中保存的局部变量超出其作用域后,这个Slot可以被复用来存储新的变量。
操作数栈操作数栈是用来执行字节码命令的,比如iadd命令在运行的时候,就是把操作数栈中最接近栈顶的2个元素出栈相加,然后将结果入栈。
操作数栈的每一个元素可以是任意的java数据类型,32位数据所占栈容量为1,64位数据所占栈容量为2。
java虚拟机的解释执行引擎是“基于栈的执行引擎”,这里的栈就是指操作数栈。
动态连接一个指向运行时常量池的引用,用来支持当前方法的代码实现动态链接。
方法返回地址一个方法开始后,只有两种方式可以退出,一种是当执行引擎遇到任意一个返回字节码指令的时候,另一种是在执行中遇到异常并且没有捕获的时候。无论以哪种方式退出,退出后都需要返回到方法被调用的位置,因此需要在栈帧中保存一些信息,用来恢复它上层方法的执行状态。
方法调用方法调用不等于方法执行,方法调用阶段的唯一目的就是确定被调用方法的版本。
解析我们知道,在虚拟机进行类加载的时候,有一个阶段叫做“解析”,在解析阶段会将常量池中的一部分符号引用转化为直接引用,在这个阶段,有一部分方法调用就已经被解析为了直接引用,解析的前提是,这部分方法的调用目标在编译阶段就可以确定下来。
在java虚拟机中共有5个方法调用指令:
invokestatic
invokespecial
invokevirtual
invokeinterface
invokedynamic
可以在“解析”阶段就确认调用目标的有invokestatic和invokespecial指令调用的方法以及invokevirtual调用的final方法,这些方法被称为“非虚方法”,与之相反的被称为“虚方法”。
分派分派分为“静态分派”和“动态分派”。
静态分派
静态分派用来实现java语言的"重载"特性。
当我们声明一个变量时,可能会用到这种形式:Human man=new Man();(Man是Human子类),对于man这个变量来说,Human叫做它的静态类型,Man叫做它的实际类型。
"重载"是基于静态类型。也就是说,对于相同名称,参数列表不同的方法,选择哪个方法取决于参数列表中参数的静态类型,而静态类型在编译时是可知的,因此静态分派发生在编译阶段。
动态分派
动态分派用来实现java语言的“重写”特性,动态分派对应invokevirtual命令。
invokevirtual命令的执行过程如下:
找到操作数栈栈顶的第一个元素所指向的对象(也被称为方法的接收者)实际类型,记为C。