第六节 执行引擎

一、字节码执行引擎概述

1、虚拟机和物理机执行引擎的区别

虚拟机和物理机都具有代码执行能力。物理机的执行引擎建立在处理器、硬件、指令集和操作系统层面的。而虚拟机的执行引擎由自己实现的,可以自行制定指令集和执行引擎结构体系,并且能够执行那些不被硬件直接支持的指令集格式。

2、JVM执行引擎

不同的虚拟机里面,可以单一或同时支持解释执行和编译执行。所有的JVM虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程字节码解析的等效过程,输出的是执行结果。

二、运行时栈帧结构

1、概述

栈帧是一种支持虚拟机进行方法调用和方法执行的数据结构,它是运行时数据区虚拟机栈的栈元素。栈帧存储的信息有:局部变量表、操作数栈、动态链接、方法返回值等。每一个方法的调用到执行结束,都对应着一个栈帧从入栈到出栈的过程。在编译程序代码时,栈帧需要多大的局部变量表、多深的操作数栈、需要分配多少内存都是完全确定的,不会受程序运行期变量数据的影响。

2、栈帧结构

第六节 执行引擎

 

三、栈帧组成

1、局部变量表(Local Variable Table)

(1)概述:用于存放方法参数和方法内部定义的局部变量。在java程序编译为Class文件时,方法的Code属性中的max_locals数据项中就确定了该方法所需要分配的局部变量表的最大容量。

(2)作用:

a、存储:局部变量表最小的单位是变量槽(Variable Slot),每个Slot都可以存放一个boolean、byte、char、short、int、float、reference或者returnAddress类型的数据。不同操作系统、处理器或者虚拟机实现的Slot大小可能不同。

b、值传递:虚拟机使用局部变量表完成参数值到参数变量列表的传递过程。实例方法第0位索引默认用于传递方法所属对象实例的引用,可以用“this”来访问这个隐含的参数。

(3)定位:虚拟机通过索引定位的方式使用局部变量表,索引范围从0开始至局部变量表最大的Slot数量。

(4)其他:对于64位的数据类型(long和double),虚拟机会采用高位对齐的方式为其分配两个连续的Slot空间,由于局部变量是建立在线程的堆栈上的,属于私有数据,不会造成读写不一致的数据安全问题。

2、操作数栈(Operand Stack)

(1)概述:后入先出(Last In First Out),同局部变量表一样,最大深度编译后固定在Code属性的max_stacks数据项中,栈中可以存储任意的Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。

(2)栈帧数据共享:概念模型中,两个栈完全独立。但实际中,虚拟机的优化处理会令两个栈出现一部分数据重叠,这样进行方法调用时无需进行传递额外的参数。Java虚拟机的解释执行引擎称为“基于栈(操作数)的执行引擎”。

 

第六节 执行引擎

3、动态连接

(1)概述:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

(2)静态解析:字节码中,方法调用指令时,以常量池中指向方法的符号引用作为参数。这些符号引用在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。

(3)动态连接:常量池中的符号引用在每一次运行期间才转化为直接引用,称为动态连接。

4、方法返回值

(1)返回方式:正常完成出口(执行引擎遇到任意一个方法返回的字节码指令)和异常完成出口(方法遇到异常且异常未被处理)。异常完成出口的方式退出不会给它的上层调用者产生任何返回值的。

(2)方法返回值:正常退出时,调用者的PC程序计数器可以作为返回地址;异常退出时,返回值地址需要通过异常处理器来确定的。

5、附加信息

这部分信息完全取决于虚拟机的实现。在实际开发中,一般会把动态连接、方法返回值与其他附加信息全部归为一类,称为栈帧信息。

四、方法调用

1、概述:方法调用并不是方法执行,方法调用的唯一目的就是确定调用哪一个方法。由于Class文件的编译中并不包含连接步骤,而且Class文件中存储的是常量池中方法的符号引用而非方法执行的内存地址(直接引用),这使得方法的调用需要在类加载期间甚至到运行期间才能确定目标方法的直接引用。

2、解析

(1)概述:在类加载的解析阶段,会将其中一部分在程序运行前就可以完全确定的符号引用转化为直接引用,这类方法的调用称为解析。

(2)可以完全确定的方法:静态方法、私有方法、实力构造器、父类方法。这几类方法不能通过继承或别的方式改写,所以在类加载时可以被解析为该方法的直接引用。

(3)调用方法的字节码指令

字节码指令

字节码含义

invokestatic

调用静态方法

invokespecial

调用示例方法构造器<init>方法、私有方法和父类方法

invokevirtual

调用所有的虚方法

invokeinterface

调用接口方法,会在运行时确定一个实现此接口的对象

invokedynamic

先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法

(4)虚方法和非虚方法

a、非虚方法:在类加载的时候就把符号引用解析为该方法的直接引用的方法。比如:静态方法、私有方法、实力构造器、父类方法、被final修饰的方法。

b、虚方法:除非虚方法外,其他的都是虚方法。比如实例方法等。

3、分派

(1)概述:解析调用在编译期完全确定,在类装载的解析阶段就全部转化为直接引用,而分派调用可能是动态的,也可能是静态的。

(2)分派分类:静态分派(单分派、多分派)、动态分派(单分派、多分派)。

(3)静态分派:所有依赖静态类型来定位方法执行版本的分派动作称为静态分派,静态分派的典型应用是方法重载。JVM(编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的,所以下面的输出并不是根据实际类型来输出的。另外需要注意的是,重载是有优先级的。解析和分派的关系并非排他关系,而是不同层次去筛选确定目标方法的过程。

 

第六节 执行引擎

(4)动态分派:动态分派不再根据静态类型来决定了。在执行第10/11/13行时,会使用到invokevirtual指令。由于invokevirtual指令执行的第一步就是在运行期间确定接收者的实际类型,所以两次调用中的invokevirtual会把类方法符号引用解析到不同的直接引用上,这个过程是Java语言中方法重写的本质。我们把这种在运行期间根据实际类型确定方法执行版本的分派过程称为动态分派。

 

第六节 执行引擎

(5)单分派和多分派:

方法的接收者与方法的参数统称为方法的宗量,单分派有一个宗量,多分派有多个宗量。

五、解释器

1、解释器的分类:

(1)字节码解释器:Java字节码 --> c++ --> 硬编码

(2)模板解释器:Java字节码 --> 硬编码

2、模板解释器原理

(1)申请一块可读可写可执行的内存;

(2)将对应操作(比如new)的硬编码写入此块内存中;

(3)将函数指针指向此硬编码所在的内存地址;

(4)调用时,通过此指针执行内存中的硬编码。

第六节 执行引擎

3、执行引擎的三种运行模式(java -Xint/Xcomp/Xmixed -version)

(1)-Xint:纯字节码解释器模式

(2)-Xcomp:纯模板解释器模式

(3)-Xmixed:字节码解释器+模板解释器(混合模式)

(4)运行效率:(纯模板解释器模式 <==> 混合解释器模式)>纯字节码解释器模式(模板解释器和混合模式的速度和程序大小有关,程序越大,混合模式速度越快;程序越小,纯模板解释器越快)

六、即时编译器(模板解释器下的编译器,无法在Mac上运行)

1、即时编译器分类

(1)C1编译器:Client模式下使用,收集的数据少,编译较浅,运行效率较低,触发条件:热点代码执行1500次以上

(2)C2编译器:Server模式下使用,收集的数据较多,编译较深,运行效率较高,触发条件:热点代码执行10 000次以上

(3)混合模式:程序运行初期使用C1编译器,运行一段时间后使用C2编译器

扩展:热点代码的单位是代码块

2、即时编译器运行原理

(1)将这个即时编译器任务写入队列中

(2)VM_THREAD从这个队列中读取任务,并运行。

扩展:即时编译的线程默认是4个,异步运行。

七、、逃逸分析

1、概念:对象作用域逃到方法之外

(1)方法逃逸:当一个对象在方法中被定义,如果它被外部的方法所访问到,称为方法逃逸。

(2)线程逃逸:当一个对象在方法中被定义,如果它被外部线程所访问到,称为线程逃逸。

2、基于逃逸分析的优化手段:栈上分配、同步消除、标量替换。

3、栈上分析(Stack Allocation):如果确定一个对象不会逃逸到方法之外,那让这个对象在栈上分配将会是一个不错的主意。这样的话,方法执行完对象的内存就被完全回收了,减轻了垃圾回收的压力。

4、同步消除(Synchronization Elimination):线程同步本身就是一个很耗时的操作,如果逃逸分析能够确定一个变量不会逃出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,那么这个变量上的同步措施(比如锁或synchronized关键字)就可以消除掉。

5、标量替换(Scalar Replancement):如果逃逸分析能够证明一个对象不会被外部访问并且这个对象可以被拆散的话,那程序执行时,可以不创建这个对象,而是根据程序访问的情况,把对象的成员变量拆分成原始类型来访问。这种方式称为标量替换。将对象拆分后,可以让对象的成员变量分配到栈上或者物理机的高速寄存器中存储。

6、总结:逃逸分析需要对数据流进行一系列的复杂分析,如果逃逸分析的收益无法高于它的消耗,那程序运行成本将会提高,目前逃逸分析技术并不成熟,所以默认不开启逃逸分析。开启逃逸分析的参数有:

(1)开启逃逸分析:-XX:+DoEscapeAnalysis

(2)查看逃逸分析结果:-XX:+PrintEscapeAnalysis

(3)开启标量替换:-XX:+EliminateAllocations

(4)查看标量替换结果:-XX:+PrintEliminateAllications

(5)开启同步消除:-XX:+EliminateLocks

八、其他概念:

1、reference类型:表示对一个对象实例的引用,通过这个引用可以做到以下两点:一是从此引用中直接或者间接地查找到对象在Java堆中的存放数据的起始地址;二是从此引用中可以直接或者间接地查找到对象所属数据类型在方法区中的存储的类型信息。

2、returnAddress类型:指向一条字节码指令的地址,很早时候用此通过指令处理异常,现已被异常表代替。

3、标量:不能再分割的数据,比如原始数据类型:int、long、reference等类型。

4、聚合量:可以再分割的数据,比如对象。