第8章 虚拟机字节码执行引擎
8.1 概述
- 执行引擎是Java虚拟机最为核心的组成部分之一。
- 在不同的虚拟机里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器生成本地代码执行)两种选择,也会有两者结合执行,甚至还会有包含不同等级编译器的执行引擎。
8.2 运行时栈帧结构
- 栈帧是用来支持虚拟机进行方法调用和方法执行的数据结构。
- 栈帧是运行时数据区中虚拟机栈中的栈元素。
- 方法从开始调用到执行完成的过程,就是栈帧在虚拟机栈中进栈和出栈的过程。
- 在编译程序代码的时候,栈帧中的需要多大的局部变量表,多深的操作数栈都已经确定好了,并且存储在方发表的Code属性中。
- 处于虚拟机栈栈顶的栈帧,称为当前栈,与该栈相关的方法称为当前方法。
栈帧中包含:局部变量表、操作数栈、动态连接、方法返回地址。
8.2.1 局部变量表
- 用来存储方法参数以及方法内部定义的局部变量。
- 局部变量表最小的单位是变量槽(Variable Slot,即Slot),虚拟机规范中没有规定一个Slot占用多大的内存空间。只是说一个Slot可以存放下一个32位以内的数据类型,例如:boolean、byte、char、short、int、float、reference或returnAddress类型的数据。
- 对于64位长度的数据类型来说(例如:long和double),虚拟机会采用高位对齐的方式为其分配2个连续的Slot空间。
8.2.2 操作数栈
- 操作数栈又称为操作栈,是一个后进先出(Last In First Out, LIFO)栈。
- 操作数栈有多深,也是在编译的时候就确定了,并且存放在方发表中的Code属性中。
- 32位的数据类型所占的栈容量为1,64的数据类型所占栈容量为2。
- 概念模型中,栈帧和栈帧之间是相互独立的,但是虚拟机在实现的时候,通常会将下边栈帧的操作数栈和上边栈帧的局部变量表重合,这样做的目的是可以避免一些复制传递的操作,如下图:
8.2.3 动态连接
-
每个栈帧中都包含一个指向运行时常量池该栈帧所属方法的引用。用于方法调用过程当中的动态连接。
-
什么是静态解析? 什么是动态连接?
在类加载过程中的解析阶段,会将部分符号引用转换为直接应用,称为静态解析。另外一部分符号引用在每次运行期间转换为直接引用,称为动态连接。
8.2.4 方法返回地址
- 方法无论以何种方式退出,都需要返回到被调用的位置,继续执行,所以栈帧中保存的方法返回地址信息,就是为了帮助恢复上层方法的执行状态。
- 一般来说,方法正常退出,可以用调用者的PC计数器的值作为方法返回地址,栈帧中很可能保存了这个PC计数器的值。异常退出,方法返回地址是通过显示处理异常表来确定的,栈帧中一般不会保存这部分信息。
8.3 方法调用
- 方法调用不等同于方法执行,它要做的事情是确定方法调用的版本(因为多态)。
- Class文件在编译过程中不存在传统编译中的连接步骤,一切方法调用在Class文件中存储的都只是一个符号引用,而不是直接引用(即方法在实际执行时内存布局中的入口地址)。
8.3.1 解析
-
在类加载的解析阶段,会将符合特定条件的符号引用转换为直接引用,这个特定的条件是:编译器在编译的时候方法就有一个可确定的调用版本,并在运行期该版本也不会发生变化。这类方法的调用称为解析(Resolution)。
这类方法包括:静态方法、私有方法、实例构造器、父类方法。这类方法也称为非虚方法,与之相反,其他方法称为虚方法(除了被final关键字修饰的方法以外)。
-
解析调用是一个静态过程, 在编译期间就可以确定方法调用的版本,所以在类加载的解析阶段会将涉及到的方法的符号引用都转换为直接引用,不会延后到运行期去完成。
8.3.2 分派
-
相对于解析调用时一个静态的过程,分派即可以是静态的也可以是动态的。
-
根据分派依据的宗量数多少,分派可以分为单分派和多分派。
-
通过静态分派、动态分派、单分派、多分派,可以两两组合4种不同的分派方式:静态单分派、静态多分派、动态单分派、动态多分派。
-
面相对象的三大特征:封装、继承、多态。
-
什么是静态分派?什么是动态分派?
- 首先我们需要知道什么是静态类型,什么是实际类型。例如:Human man = new Man(),这里的Human就是静态类型,Man就是实际类型。
- 在编译期基于静态类型来确定方法调用版本的分派,称为静态分派,典型应用是:方法重载。静态分派是在编译期发生的,所以静态分派这个动作不是虚拟机来完成的。
- 在运行期基于实际类型来确定方法调用版本的分派,称为动态分派,典型应用是:方法重写。
-
什么是单分派?什么是多分派?
-
首选我们需要了解两个概念接收者和宗量:
接收者:以 User u = new User(); u.sayHello() 代码为例,其中对象u是方sayHello()的所有者,则对象u称为接收者(Receiver)。
宗量:方法的接收者和参数统称为方法的宗量。
-
根据1个宗量来确定方法调用版本称为单分派。
-
根据多个宗量来确定方法调用版本称为多分派。
-
-
动态分派如何实现的?
动态分派的方法版本确定过程需要在运行期在类的方法元数据中搜索合适的目标方法,基于效率的考虑,虚拟机采用的是在方法区建立一个虚方法表(Vritual Method Table,也称为vtable),以此替代在方法元数据中搜索。虚方法表中存放的是各个方法在内存布局中的实际入口地址。
如果子类中没有对父类方法进行重写,则该方法在子类的虚方法中存放的入口地址和父类的方法的入口地址一致。反之,如果方法重写了,则该方法在子类的虚方法表中存放的就是指向子类实现版本的入口地址。
8.4 基于栈的字节码解释执行引擎
- 大部分程序代码到物理机的目标代码或虚拟机可执行的指令集之前,都需要经过下图中的各个步骤:
- 在Java语言中,Javac编译器的编译过程是:源码经过了词法分析、语法分析到生成抽象语法树(Abstract Syntax Tree,AST),再遍历抽象语法树生成线性的字节码指令流。这个过程是在虚拟机之外执行的,所以Java程序的编译器是半独立实现的,而解释器是在虚拟机内部的。
- 指令集架构分为:基于栈、基于寄存器。
-
基于栈的指令集
Java编译器输出的字节码指令流,基本上是基于栈的指令集架构(Instruction Set Architecture,ISA),指令流中的指令大都是零地址指令,它们需要依赖操作数栈进行工作。优点是可移植性好,缺点是执行效率低。
-
基于寄存器的指令集
最典型的就是x86的二地址指令集,通俗点来说,就是目前主流PC机所支持的指令集架构。由于要与硬件绑定,所以可移植性差,但是执行效率高。
-
两者的区别
基于栈的指令集中的指令只包含操作码,操作数是在栈帧中的操作数栈中存储的。
基于寄存器的指令集中的指令包含了操作码和操作数两部分。
-
上一篇:《深入理解JAVA虚拟机(第2版)》- 第7章 - 学习笔记
下一篇:《深入理解JAVA虚拟机(第2版)》- 第10章 - 学习笔记