java虚拟机读书笔记 第八章 虚拟机字节码执行引擎

203 阅读8分钟

java虚拟机规范里制定了虚拟机字节码执行引擎的概念模型。在不同的虚拟机实现里,执行引擎在执行java代码的时候可能会有解释执行和编译执行。但从外观上来看,所有java虚拟机的执行引擎是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果

运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

局部变量表

局部变量表是一组变量值存储,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为class文件时,就在code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。 局部变量表的容量以变量槽Slot为最小单位,每个Slot都应用能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据(java中占用32位以内的8中类型)。对于64位的数据类型,会采用高位对齐的方式分配两个连续的Slot空间。局部变量建立在线程的堆栈上是线程私有数据。连续写入Slot不会引起线程安全。 虚拟机通过索引的方式访问局部变量表,索引值的访问从0开始。 在方法执行时,虚拟机是使用局部变量表完成参数值到参数列表的传递过程的,如果执行的是实例方法,局部变量表的第0位索引默认是传递方法所属的实例对象,通过this来访问。为了节省空间,Slot是可以复用的,但某些情况下会影响到gc。 局部变量不像类变量存在准备阶段,如果一个局部变量定义了但是没有赋初始值是不能使用的。

操作数栈

它是后入先出(LIFO)栈。同局部变量,操作数栈的栈深度也在编译的时候写入到了Code属性的max_stacks数据项中。操作数栈中的每一个元素可以是任意的java数据类型,包括long和double。在方法执行的任何时候,操作数栈的深度都不能超出max_stack数据项中设定的最大值。 Java虚拟机的解释执行引擎称为基于栈的执行引擎,其中所指的栈就是操作数栈。

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中动态连接。字节码中方法的调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转换为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

##方法返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这种退出方式称为方法的正常完成出口。另一中退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体中正确的得到处理,这种退出方法的方式称为异常完成出口,通过这种方式退出,不会给它的上层调用返回任何值。方法正常退出时,调用者的pc计数器的值可以作为返回地址。而方法异常退出时,返回地址是要通过异常处理器表来确定的。 方法退出的过程实质上就是把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者操作数栈中,调整pc计数器的值以指向下一条指令。 一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。

方法调用

方法调用的阶段的唯一任务就是确定被调用方法的版本(即调用哪一个方法)。

解析

所有方法调用的目标在Class文件里面都是一个常量池中的符号引用,在解析阶段会把符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正能运行之前就有一个可以确定的调用版本,并且这个版本在方法的调用阶段是不可改变的。这类方法的调用称为解析。

java中符合编译期可知,运行期不可变的,主要包括:静态方法和私有方法两大类。前者于类型关联,后者在外部无法访问,都不可能通过继承或别的方式重写其他版本,适合在类加载阶段进行解析。

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法,他们在类加载的时候就会把符号引用转化为直接引用。这些方法称为非虚方法,其他方法称为虚方法(不包括final)。虽然final方法是使用invokevirtual指令调用的,但是它无法被覆盖。java语言规范中明确说明final方法是一种非虚方法。

分派

解析调用一定是个静态的过程,在编译期可知,在类加载的解析阶段就会把涉及到的符号引用转化为直接引用,不会延迟到运行期再去完成。而分派调用则可能静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派,所以分派共有4种情况:静态单分派、静态多分派、动态单分派、动态多分派。

静态分派

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法的重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。

动态分派

它和多态性的重写有着很密切的联系。重载是静态的,重写是动态的。

invokevirtual指令的运行时解析过程大致分为以下几个步骤: 1)找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。 2)如果在类型C中找到的与常量中的描述符和简单名称都相符合的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找结束;如果不通过,则返回java.lang.IllegalAccessError异常。 3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。 4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

单分派与多分派

方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多个宗量对目标方法进行选择。今天的java语言是一门静态多分派、动态单分派的语言。

虚拟机动态分派的实现

由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法。面对这种情况,最常用的稳定优化手段就是为类在方法区中建立一个虚方法表。使用虚方法表索引来代替元数据查找以提高性能。虚方法表中存放着各个方法的实际入口地址。