3.3 虚拟机执行子系统--虚拟机字节码执行引擎

虚拟机执行子系统

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

   第六、七章讲解了如何在Class文件中定义类,如何将类加载到虚拟机中,本章主要讲解虚拟机如何执行定义在Class文件里的字节码。

  所有Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。


一、运行时栈桢结构

  栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素栈帧存储了方法的局部变量表操作数栈动态连接方法返回地址等信息每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。一个栈桢需要分配多少内存不受程序运行期间变量数据的影响,仅仅取决于具体的虚拟机实现。

  在活动线程中,只有位于栈顶的栈桢才是有效的,称为当前栈帧,与这个栈桢相关联的方法称为当前方法。

 1.局部变量表

  局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译成Class文件时,就确定了该方法所需要的局部变量表的最大容量。

  局部变量表容量以变量槽(Slot)为最小单位,每个Slot都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存放。对于64位的数据类型,例如long和double,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。

  第7种reference类型表示对一个对象实例的引用,虚拟机至少都应当能通过这个引用做到两点,一是从此引用中直接或间接地查找到对象在Java堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,否则无法实现Java语言规范中定义的语法约束约束。第8种即returnAddress类型目前已经很少见了,它是为字节码指令jsr、jsr_w和ret服务的,指向了一条字节码指令的地址,很古老的Java虚拟机曾经使用这几条指令来实现异常处理,现在已经由异常表代替。
 
2.操作数栈
  操作数栈也成为操作栈,32位数据所占的栈容量为1,64位数据所占的容量位2。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。

  在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无须进行额外的参数复制传递。

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

4.方法返回地址

  当一个方法开始执行后,只有两种方式可以退出这个方法

  第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口

  另外一种退出方式是,在方法执行过程中遇到了异常。

5.附加信息

……略


二、方法调用

  方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。

1.解析

  继续前面关于方法调用的话题,所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来这类方法的调用称为解析(Resolution)。

  在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析

  解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用,不会延迟到运行期再去完成。

  而分派调用则可能是静态的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况,下面我们再看看虚拟机中的方法分派是如何进行的。

2.分派调用

  详情点击


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

  本节讨论虚拟机是如何执行方法中的字节码指令的。

1.解释执行

  大部分的程序代码到物理机的目标代码或虚拟机能执行的指令集之前,都需要经过下图中的各个步骤。

   Java语言中,Javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树再遍历语法树生成线性的字节码指令流的过程。因为这一部分动作是在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以Java程序的编译就是半独立的实现。

2.基于栈的指令集与基于寄存器的指令集

  Java编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流中的指令大部分都是零地址指令,它们依赖操作数栈进行工作。

  基于栈的指令集主要的优点就是可移植,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。

  栈架构指令集的主要缺点是执行速度相对来说会稍慢一些。


原文地址:https://www.cnblogs.com/qmillet/p/12051696.html