执行引擎是 Java 虚拟机最核心的组成部分之一
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性之中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
局部变量
**局部变量表是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。
局部变量表的容量以变量槽(Variable Slot)为最小单位。** 一个Slot可以存放一个32位以内(boolean、byte、char、short、int、float、reference和returnAddress)的数据类型,reference类型表示一个对象实例的引用,returnAddress已经很少见了,可以忽略。
对于64位的数据类型(Java语言中明确的64位数据类型只有long和double),虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。
虚拟机通过索引定位的方式使用局部变量表,索引值的范围从0开始至局部变量表最大的Slot数量。访问的是32位数据类型的变量,索引n就代表了使用第n个Slot,如果是64位数据类型,就代表会同时使用n和n+1这两个Slot。
为了节省栈帧空间,局部变量Slot可以重用,方法体中定义的变量,其作用域并不一定会覆盖整个方法体。如果当前字节码PC计数器的值超出了某个变量的作用域,那么这个变量的Slot就可以交给其他变量使用。这样的设计会带来一些额外的副作用,比如:在某些情况下,Slot的复用会直接影响到系统的收集行为。
操作数栈
1.概念
一个先入后出的栈,其最大深度在编译的时候写入到了方法Code属性的数据项max_stacks中。32位的数据类型所占栈容量为1,64位数据类型的栈容量为2。在方法执行的时候,操作数栈的深度不会超过在max_stacks中设定的最大值。
操作数栈中元素的数据类型必须与字节码指令要求的类型严格匹配。
如果两个栈帧之间的操作数栈局部变量表存在共用数据,那么允许这两个局部变量表重叠,无需进行额外的参数复制传递。
动态链接
字节码的方法调用指令以常量池中的指向方法的符号引用作为参数,如果符号引用在每一次运行期间转为符号引用,就称为动态链接。
方法返回地址
当一个方法开始执行时,有两种退出方法的方式:①第一种:执行引擎遇到任意一个方法返回的字节码指令(如ireturn),这种称之为正常完成退出。
第二种方式是在方法执行过程中遇到异常,并且这个异常没有在方法体内得到处理,无论是虚拟机内部产生的异常,还是代码使用athrow字节码指令产生的异常,只要是在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。这种称之为异常完成出口。
在方法退出之后,需要返回到方法被调用的位置,即调用点,继续执行其余程序。方法返回时可能需要在栈帧中保存一些信息,用于帮助其恢复它的上层方法的执行状态。方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时要通过异常处理器表确定,栈帧中不会保存这部分信息。
注意:方法退出等于把当前栈帧出栈,退出时操作有:恢复上层调用方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令的后面一条指令。
方法调用
方法调用不等于方法执行,方法调用阶段的任务是确定被调用方法的版本(即调用哪一个方法)
解析
1.所有方法调用中的目标方法在Class文件里面都是一个常量池的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的条件是方法在程序真正运行之前就有一个可确定的版本,并且这个方法的调用版本在运行期是不可变的。即调用目标在程序代码写好、编译器进行编译时就必须确定下来。这种调用称为解析。
2.符合”编译器可知,运行期不变“的方法,主要是静态方法和私有方法。静态方法与类型直接相关,私有方法在外部不可能被访问,因此这两种方法不可能通过继承或别的方法重写其他版本,因此适合在类加载阶段进行解析。
3.方法调用的字节码指令有invokestatic invokespecial invokevirtual invokeinterface invokedynamic
。只要能被invokestatic invokespecial
指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合条件的有静态方法、私有方法、实力构造器、父类方法4类。它们在类加载的时候就会把符号引用解析为该方法的直接引用。 这些方法称为非虚方法。final
修饰的方法也是非虚方法。
4.解析与分派的比较
解析是个静态的过程,编译期间就完全确定,在类加载的解析阶段会把涉及的符号引用全部转为直接引用,不会延迟到运行期再去完成。
分派可能是静态的,也可能是动态的。根据分派依据的宗量数可分为单分派与多分派,所以分派方式一共有4种:静态单分派、静态多分派、动态单分派、动态多分派。
分派
分派显示了多态性特征。如重载、重写。
基于栈的字节码解释执行引擎
Java语言中,Javac编译器(虚拟机外部)完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。而解释器在虚拟机内部,所以Java的编译是半独立的编译