JVM字节码执行引擎
运行时栈帧结构
局部变量表
操做数栈
动态链接
方法返回地址
附加信息
方法调用
解析
分派 –“重载”和“重写”的实现
静态分派
动态分派
单分派和多分派
JVM动态分派的实现
基于栈的字节码解释执行引擎
基于栈的指令集与基于寄存器的指令集程序员
虚拟机是相对于“物理机”而言的,这两种机器都有代码执行能力,其区别主要是物理机的执行引擎是直接创建在处理器、硬件、指令集和操做系统层面上的,而虚拟机的执行引擎是本身实现的。所以程序员能够自行制定指令集和执行引擎的结构体系,而且可以执行那些不被硬件直接支持的指令集格式。
在Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型称为各类虚拟机执行引擎的统一外观。虚拟机实现中,可能会有两种的执行方式:解释执行(经过解释器执行)和编译执行(经过即时编译器产生本地代码)。有些虚拟机值采用一种执行方式,可是有点采用了两种,甚至有可能包含几个不一样级别的编译器执行引擎。
全部的Java虚拟机的执行引擎都是一致的:输入的是字节码文件、处理过程是等效字节码解析过程,输出的是执行结果。安全
栈帧(Stack Frame)是一种数据结构,它主要是用来支持虚拟机进行方法调用和方法执行。它是虚拟机运行时数据区的虚拟机栈的栈元素。
包含内容:栈帧包含了局部变量表、操做数栈、动态链接、方法返回地址和一些额外的附加信息等。
执行过程:一个线程中的方法调用链可能会很长,不少方法都同时处于执行状态。在活动线程中,只有栈顶的栈帧才是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法,执行引擎所运行的全部的字节码指令都只针对当前栈帧进行操做。
执行意义:每一个方法从调用开始到执行完成的过程,就对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。数据结构
值得注意的是:在编译程序代码的时候,栈帧须要多大的局部变量表、多深的操做数栈都已经彻底肯定了,而且写入到方法表的Code属性之中,所以一个栈帧须要分配多大的内存,并不会受到运行期变量数据的影响,而仅仅取决于具体的虚拟机的实现。架构
一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件时,就在方法的Code属性的max_locals数据项中肯定了改方法所需分配的最大局部变量表的容器。
包含类型:boolean、byte、char、short、int、float、reference或returnAddress类型八种类型。
容量单位:变量槽(slot)。不过虚拟机中并无明确肯定每个变量槽所占据的内存空间大小,只是有导向性地说明每一个变量槽都应该存放的八种类型:boolean、byte、char、short、int、float、reference或returnAddress类型的数据。这种描述和明确指出“每一个Slot占用32位长度的内存空间”有一些差异,它容许Slot的长度随着不一样的处理器、操做系统或者虚拟机而发生改变。在64位系统上使用64位长度的内存空间来实现一个slot,虚拟机仍要使用对齐和补白的手段让Slot在外观上看起来与32位虚拟机中的一致。布局
在Java中占32位之内的数据类型有boolean、byte、char、short、int、float、reference或returnAddress类型等,前六种不解释,然后面的reference是对象的引用。虚拟机规范并无说明它的长度,也没有明确指出这个引用应有怎样的结构,但通常来讲:虚拟机实现至少都应当能今后引用中直接或间接地查找到对象在Java堆中的起始地址索引和方法区中的对象类型数据。而returnAddress是为字节码指令jsr、jsr_w和ret服务的,它指向一条字节码指令的地址。
对于64为的数据类型,虚拟机会以高位在前的方式为其分配两个连续的Slot空间。即long和double两种类型。作法是将long和double类型速写分割为32位读写的作法。不过因为局部变量表创建在线程的堆栈上,是线程的私有数据,不管读写两个连续的Slot是不是原子操做,都不会引发数据安全问题。性能
虚拟机索引方式:虚拟机经过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的Slot数量。若是是32为数据类型的数据,索引n就表示使用第n个Slot,若是是64位数据类型的变量,则说明要使用第n和第n+1两个Slot。
在方法执行过程当中,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程。若是是实例方法(非static方法),那么局部变量表中的第0位索引的Slot默认是用来传递方法所属对象实例的引用,在方法中能够经过关键字“this”来访问这个隐含的参数。其他参数按照参数表的顺序来排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和做用域分配其他的Slot。
局部变量表中的Slot是可重用的,方法体中定义的变量,其做用域并不必定会覆盖整个方法体,若是当前字节码PC计数器的值已经超过了某个变量的做用域,那么这个变量相应的Slot就能够交给其余变量去使用。节省栈空间。但也有可能会影响到系统的垃圾收集行为。优化
还有一点要说明的是:局部变量不像前面介绍的类变量那样存在“准备阶段”。咱们知道,类变量在加载过程当中要通过两次赋初始值的过程:一次在准备阶段,赋予系统初始值,另一次在初始化阶段,赋予程序员定义的初始值。但局部变量不同,若是一个局部变量定义了可是没有赋初始值是不能使用的。全部不要认为Java中任何状况下都存在着诸如整型变量默认为0,布尔型变量默认为false之类的默认值。这一点要好好注意一下。this
操做栈,它是一个后入先出栈。同局部变量表同样,操做数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。
操做数栈的每个元素能够是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位所占的栈容量为2.在方法执行的任什么时候候,操做数栈的深度都不会超过在max_stacks数据项中设定的最大值。spa
当一个方法刚刚开始执行的时候,这个方法的操做数栈是空的,在方法的执行过程当中,会有各类字节码指令向操做数栈中写入和提取内容,也就是入栈出栈操做。
操做数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。
另外,在概念模型中,两个栈帧做为虚拟机栈的元素,相互之间是彻底独立的。可是大多数的虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。这样在进行方法调用时就能够共用一部分数据,而无须进行额外的参数复制传递。操作系统
Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中的栈就是指操做数栈。
每一个栈帧都包含着一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用的是为了支持方法调用过程当中的动态链接。
在Class文件中存在着大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分在类加载阶段第一次使用阶段的时候转换为直接引用,这种转换称为静态解析。另一部分将在每次的运行期间转化为直接引用,这部分称为动态转换。
当一个方法被执行后,有两种方式能够退出这个方法。
第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。
另一种退出方式是:在方法执行过程当中遇到异常,而且这个异常没有在方法体内获得处理,不管是JVM内部产生的异常,仍是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会致使方法退出。这种方式被称为异常退出出口。此方式不会给上层调用者产生任何返回值。
不管采用哪种退出方式,在方法退出后,都会返回到方法被调用的位置,程序才能继续执行。方法返回时可能要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。通常来讲,方法正常退出以后,调用者的PC计数器的值就能够做为返回地址。栈帧中极可能会保存这个计数器值,而方法异常退出后,返回地址就要经过异常处理器表来肯定,栈帧通常不保存这部分信息。
方法退出实际上就是把当前栈帧出栈的操做:所以退出时可能执行的操做:恢复上层方法局部变量表和操做数栈,把返回值压入调用者栈帧的操做数栈中,调整PC计数器的值以指向指令后面的一条指令。
增长一些没有描述的信息到栈帧之中。通常将动态链接、方法返回地址和其余附加信息所有归为一类,称为栈帧信息。
Class文件的编译过程当中不包含传统编译中的链接步骤,一切方法调用都在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(至关于以前所说的直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂起来,须要在类加载期间,甚至到运行期间才能肯定目标方法的直接引用。
全部的方法调用的目标方法在Class文件里面都只是一个常量池的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是L方法在程序真正运行以前就有一个可肯定的调用版本,而且这个方法的调用版本在运行期是不会改变的。换句话说:调用目标在程序代码写好、编译器进行编译时就必须肯定下来,这类方法的调用称为解析。
在JVM中提供了5条方法调用字节码指令,分别是:
invokestatic:调用静态方法
involespecial:调用实例构造器方法、私有方法和父类方法。
invokevirtual:调用全部的虚方法。
invokeinterface:调用接口方法,会在运行时再肯定一个实现此接口的对象。
invokedynamic:先在运行时动态解析出调用限定符所引用的方法,而后再执行该方法。
只要能被invokestatic和invokeapecial指令调用的方法,都是能够在解析阶段肯定惟一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类,他们在类加载的时候就会把符号引用解析为该方法的直接引用,这些方法能够称为非虚方法,与之相反,其余方法称为虚方法(除去final方法)。非虚方法除了上述的两种之外,还有一种就是被final修饰的方法,虽然final方法是使用invokevirtual指令来调用的,可是因为它没法被覆盖,因此能够把final方法看做是一种非虚方法。
解析调用必定是个静态的过程,在编译期间就能够肯定,在类装载的解析阶段就会把涉及的符号引用所有转变为可肯定的直接引用,不会延迟到运行期再去完成。而分派调用则多是静态的或者是动态的,根据分派依据的总量数能够分为单分派和多分派,这两种分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派和动态多分派四种分派组合。
JVM在重载时是经过参数的静态类型而不是实际类型作断定的,而且静态类型是编译期可知的,所以在编译阶段,Javac编译器会根据参数的静态类型决定使用那个重载版本,而后再把该方法的符号引号写到main()方法的两条invokevritual指令的参数中。
全部依赖于静态类型来定位方法执行版本的分派动做称为静态分派,静态分派的典型是方法的重载。
静态分派发生的时间:静态分派发生在编译阶段,所以肯定静态分派的动做实际上不是由虚拟机来执行的。
静态方法会在类加载期进行解析,而静态方法显然是能够拥有重载版本的,选择重载版本的过程也是经过静态分派完成的。
运行期根据实际类型肯定方法执行版本的分派过程称为动态分派。动态分派与方法重写有着密切的关系。
方法的接收者与方法的参数统称为方法的宗量。而根据分派基于多少种宗量,能够将分派划分为单分派和多分派两种,单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。
因为动态分派是很是频繁的动做,并且动态分派的方法版本选择过程须要运行时在类的方法元数据中搜索合适的目标方法,为了不频繁的搜索,最经常使用的“稳定优化”的手段就是为类在方法区中创建一个虚方法表,使用虚方法表索引来代替元数据查找以提升性能。
虚方法表中存放着各个方法的实际入口地址,若是某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都是指向父类的实现入口,若是子类中重写了这个方法,子类方法表中的地址将会替换成指向子类实现版本的入口地址。
为了程序实现上的方便,具备相同的签名的方法,在父类、子类的虚方法表中都应当具备同样的索引序号,这样当类型变换时,仅须要变动在、查找的方法表,就能够从不一样的虚方法表中按照索引转换出所需的入口地址。
Java程序在执行前先对程序源码进行词法分析和语法分析处理,把源码转化为抽象语法树。对于一门具体语言的实现来讲,词法分析、语法分析以及后面的优化器和目标代码生成器均可以选择独立于执行引擎,造成一个完整意义的编译器去实现,这类表明是C/C++语言。固然也能够选择其中的一部分步骤实现一个半独立的编译器,这类表明是Java语言。又或者把这些步骤和执行引擎所有集中封装到一个封闭黑匣子中,如大多数的JS执行器。
Java编译器输出的指令流,基本上是一种基于栈指令集架构,指令流中的指令大部分都是零地址指令,它们依赖操做数栈进行工做。
基于栈的指令集主要优势就是可移植。除此以外,还有其余的优势,如代码相对更加紧凑(字节码中每一个字节就对应一条指令,而多地址指令集中还须要存放参数)、编译器实现更加简单等。 缺点是:执行速度相对较慢。