虚拟机的执行引擎可以指定指令集,执行那些不能够被硬件系统直接支持的指令集格式。
Engine执行代码时一般分为两种类型:
- 解释执行 传统方式
- 编译执行(e.g JIT),产生本地机器码,编译花费时间多,但是执行时效率和速度更高
栈帧
在线程的JVM Stack中使用,用于支持JVM进行方法调用和方法执行的数据结构
- 包括了局部变量表、方法返回地址、操作数栈、动态链接和附加信息。由于在编译Java时,需要多大空间的局部变量表和操作数栈已经确定,因此栈帧占用的内存不会受到运行时间变量数据的影响。
- 对Engine来说,在活动线程中,只有处于栈顶的栈帧(Current Stack Frame)才是有效的,所有的字节码指令都只对都只作用在Current Stack Frame关联的方法 (Current Method).
局部变量表
一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。 局部变量的容量以变量槽(Slot)为最小单位。
- 每一个Slot占用的内存空间并没有指定,根据不同平台的JVM而已。但一般都可以存放32位以内的数据类型:boolean(1), byte(8), char(16), short(16), int(32), float(32), reference (对象引用) 和 returnAddress。对于reference, JVM必须要做到能够直接或间接查找到对象在Java堆中的数据存放地址起始索引和查找到对象所属数据类型在方法区中的存储的类型信息。
- 在方法执行时,JVM使用局部变量表完成 参数值 到 参数变量列表 的传递。第0位索引的Slot默认是用于传递方法所属对象实例的引用(this),其余参数则按照参数表顺序排列,后面再根据方法体内部定义的变量顺序和作用域分配其余的Slot.
操作数栈
LIFO栈,操作数栈中的每一个元素可以是在任意的Java数据类型,32位的占用栈容量1,64位的占用栈容量2。Engine执行过程其实就是从操作数栈从提取元素然后执行指令,然后再将执行的结果压入栈中。
动态链接
每一个栈帧都包含有一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。
字节码的方法调用指令就是以Class文件的常量池中指向方法的符号引用作为参数;这些符号引用在类加载或者第一次使用的时候就转为了直接引用,这种方式称为静态解析;而有点符号链接会在每一次运行期间转化为直接引用,称为动态链接。
方法返回地址
一个方法执行完成之后,只有两种方式可以退出这个方法。第一种方式是遇到任意一个方法返回的字节码指令,然后将返回值(如果有的话)返回给上层的方法调用者,这种方式称为Normal Method Invocation Completion;而另外一种方式是在方法执行过程中遇到了异常,同时异常没有在方法体内部得到处理从而导致方法退出,该种方式称为Abrupt Method Invocation Completion。
方法退出的过程实际上就是将当前栈帧出栈,因此退出时可能的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令。
方法调用
由上面提到的静态解析我们得知,有一些方法调用指令的符号引用在类加载或者第一次使用时就已经转化为了直接引用,这种方法基于一个前提:方法在真正运行之前就有一个可确定的调用版本,并且该版本在运行时是不会改变的。
这种方法在JVM规范里也称为“非虚方法”,符合这个前提条件的方法有:static修饰的静态方法、private 修饰的私有方法、类的实例构造器<init>、父类方法和有final 修饰的方法。
其根本原因是这些方法在程序运行期间不能被通过任何方式重写为其他的版本,因此在编译期间便可以将他们确定下来。
Method Overload Resolution
- Static
当我们在Java中声明一个引用时会给他附上一个类型,该类型称为变量的Static Type,而当使用某个Class(类型)实例为一个对象时,该类型也称之为变量的Actual Type。由于在编译时,程序代码已经确定,因此Static Type在编译期是可知的(声明变量,强制类型转换);但是Actual Type在运行时才能够确定下来。
因此JVM的Compiler在进行Method Overloading时是以参数的静态类型作为判断依据的。 - Dynamic
Java的多态性核心之一重载的本质我们已经在上面的静态分派中有所了解,多态的另一个重要功能——重写,便同动态分派息息相关。
通过观察Class的字节码,可以发现,在执行方法时,JVM会先从操作数栈中提取出方法的执行者(Receiver),然后在其之上调用invokevirtual指令(将指向Class文件常量池中的方法符号引用解析到不同的直接引用上)。
因此,真正执行方法的版本其实是根据Receiver来确定的,换句话说就是根据对象的实际类型来确定。
第一步先确认方法执行者Receiver的实际类型,在第二步进行方法符号引用解析时根据Receiver实际类型的不同将符号引用解析到不同的直接引用上。这既是虚方法的调用过程,亦是Java语言方法重写(Method Overwriting)的本质。