JVM字节码执行引擎
一、概述
在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译器执行(通过即时编译器产生本地代码执行)两种选择,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。
每个字节码指令都由一个1字节的操作码和附加的操作数组成。
二、运行时栈帧结构
栈帧(Frame Frame)是用于支持虚拟机运行方法调用和执行的数据结构,每一个栈帧都包括了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性中。
重点理解:栈帧属于线程,每个线程中一般很多方法都同时处于执行状态。对于执行引擎来讲,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与之相关联的方法称为当前方法(即每一个方法都拥有自己独立的局部变量表、操作数栈、动态链接和方法的返回地址等信息)。
2.1 局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
类变量有两次赋初始值的过程,一次是准备阶段,赋予系统初始值,整型 = 0,布尔类型 = false;另一次是初始化阶段,赋予程序员定义的初始值。因此即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但是局部变量不一样,如果一个局部变量定义了但没有赋初始值是不能使用的,字节码校验的时候也会被虚拟机发现而导致类加载失败!
public static void main(String[] args) { int value; System.out.println(value); //程序编译失败,未给局部变量附初始值 }
2.2 操作数栈
操作数栈是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法表的 Code 属性的 max_locals 数据项中。操作数栈的每一个元素可以是任意的 Java 数据类型,包括 long 和 double;
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。
2.3 动态链接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,只有这个引用是为了支持方法调用过程中的动态链接(Dynamic Linking)。Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态链接。另外一部分将在每一次运行期间转化为直接引用,这部分称为动态链接!
当前线程的栈帧通过获取方法的直接引用,指向着常量池对应方法的字节码,就可以利用常量池、操作数栈执行方法!
2.4 方法返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法。
第一种:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;
第二种:在方法的执行过程中遇到了异常(Exception),并且议程没有在方法体中处理,简称异常完成出口;
无论何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。
三、方法调用
方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普遍、最频繁的操作,但Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于上面说的直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂起来,需要在类加载期间,设置到运行期间再能确定目标方法的直接引用!
3.1 解析
只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以称为非虚方法。
3.2 静态分派和动态分派
首先明白一点:Java语言是一种静态多分派,动态单分派语言!
静态分派(方法重载关联)
变量本身的静态类型不会被改变,静态类型在编译期可知,而实际类型变化的结果在运行期才可确定。静态方法会在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程也是通过静态分派完成的。
动态分派(方法的重写关联)
invokevirtual指令的多态查找过程,运行时解析过程分为:
由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此在虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正地进行如此频繁的搜索。面对这种情况,最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表(Virtual Method Table,也称为itable),使用虚拟机表索引来代替元数据以提高性能!
四、基于栈的字节码解释执行引擎
4.1 解释执行
Java语言经常被人定义为“解释执行”的语言!
编译原理的简单过程:词法分析 --> 语法分析 --> 语义分析和中间代码的产生 --> 优化 --> 目标代码生成!
Java语言中,javac 编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。这一部分动作是在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以Java程序的编译就是半独立的实现!
4.2 基于栈的指令集
4.3 基于栈的解释器执行过程
一段简单的算术代码:
public int calc(){ int a = 100; int b = 200; int c = 300; return (a + b) * c; }
字节码指令表示:
public int calc();
Code:
Stack=2, Locals=4, Args_size=1 //操作栈深度为2和4个Slot局部变量表
0:bipush 100 //将100压入操作数栈
2:istore_1 //将栈顶100数值存放到局变量Slot,index=1中
3:sipush 200 //将200压入操作数栈
6:istore_2 //将栈顶200数值存放到局部变量Slot,index=2中
7:sipush 300 //将300压入操作数栈
10:istore_3 //将栈顶200数值存放到局部变量Slot,index=3中
11:iload_1 //将index=1的局部变量表数值压入操作数栈(100)
12:iload_2 //将index=2的局部变量表数值压入操作数栈(200)
13:iadd //取栈顶两个数值相加,结果压入操作数栈(300)
14:iload_3 //将index=3的局部变量表数值压入操作数栈(300)
15:imul //取栈顶两个数值相乘,结果压入操作数栈(90000)
16:ireturn //取栈顶数值返回调用者结果
图解展示: