虚拟机字节码执行引擎(第八章)

139 阅读6分钟

概述

执行引擎时Java虚拟机核心的组成部分之一。“虚拟机”时一个相对于“物理机”的概念。这两种机器都有代码执行能力,其区别时物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集和执行引擎的结构体系,能够执行哪些不被硬件直接支持的指令集格式。

在不同的虚拟机实现中,执行引擎在执行字节码的时候,通常会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可两者兼备,还可能会有同事包含几个不同级别的即时编译器一起工作的执行引擎。所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。

运行时栈帧结构

局部变量表

局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,它的容量以变量槽为最小单位。在Java程序被编译为class文件时,就在方法的code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。

虚拟机实现至少都应当能通过引用做到两件事,一是从根据引用直接或间接地查找到对象在Java堆中的数据存在的起始地址或索引,二是根据引用直接或间接地查找 到对象所属数据类型在方法区中的存储的数据类型。

操作数栈

操作数栈也常称为操作栈,它是一个先进后出栈。同局部变量一样,操作数栈的最大深度也在编译的时候被写入到code属性的max_stacks数据项中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。

当一个方法刚开始执行的是,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈的过程。Java迅即的解释执行被称为“基于栈的执行引擎”,里面的“栈”就是操作数栈。

动态连接

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

方法返回地址

当一个方法开始执行后,只有两种方式退出这个方法。第一种是执行引擎遇到任意个方法返回的字节码指令,这个时候可能会有返回值传递给上层的方法调用者,这种退出方法的方式称为“正常调用完成”。另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理,这种退出方式称为“异常调用完成”。

方法调用

方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未设计方法内部的具体运行过程。class文件的编译过程中不包含传统程序语言编译的连接步骤,一切方法调用在class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(即直接引用)。某些调用需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。

解析

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

分派

静态分派(重载)

《深入Java虚拟机》第8章304页代码实例中,"Human"成为变量的“静态类型”,后面的“man”称为变量的“实际类型”。静态类型和实际类型在程序中都可能会发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译器可知的;而实际类型变化的结果在运行期才可确定。所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。

动态分派(重写)

由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。我们把这种运行在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

单分派与多分派

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

虚拟机动态分派的实现

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类没有被重写,那么子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口;如果子类重写了这个方法,子类方法表中的地址将会转换为指向子类实现版本的入口地址

动态类型语言支持

动态语言类型

Java与动态类型

java.lang.invoke包

invokedynamic指令

基于栈的字节码解释执行引擎

解释执行

基于栈的指令集与机遇寄存器的指令集

基于栈的解释器执行过程

\