代码编译的结果从本地机器转变为字节码,是存储格式发展的一小步,倒是编程语言发展的一大步。编程
执行引擎是Java虚拟机最核心的组成部分之一。虚拟机是一个相对于物理机的概念,这两种机器都有代码执行的能力,其区别是物理机的执行引擎是直接创建在处理器、硬件、指令集和操做系统层面上的,而虚拟机的执行引擎则是由本身实现的,所以能够自行制定指令集与执行引擎的结构体系,而且可以执行那些不被硬件直接支持的指令集格式。数据结构
在Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各类虚拟机执行引擎的统一外观。在不一样的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(经过解释器执行)和编译执行(经过即时编译器产生本地代码执行)两种选择,也可能二者兼备,甚至还可能会包含几个不一样级别的编译器执行引擎。可是从外观上看起来,全部的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。编程语言
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操做数栈、动态链接和方法返回地址等信息。每个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。spa
每个栈帧都包括了局部变量表、操做数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码的时候,栈帧中须要多大的局部变量表、多深的操做数栈都已经彻底肯定了,而且写入到方法表的Code属性之中,所以一个栈帧须要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。操作系统
一个线程中的方法调用链可能会很长,不少方法都同时处于执行状态。对于执行引擎来讲,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。执行引擎运行的全部字节码指令都只针对当前栈帧进行操做,典型的栈帧结构以下图所示:线程
接下来说解栈帧中的局部变量表、操做数栈、动态链接、返回地址等各个部分的做用和数据结构。调试
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中肯定了该方法所须要分配的局部变量表的最大容量。对象
局部变量表的容量以变量槽(Variable Slot)为最小单位,一个Slot(变量槽)能够存放一个32位之内的数据类型,Java中占用32之内的数据类型有boolean、byte、char、short、int、float、reference和 returnAddress 8中类型。对于64位的数据类型,虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。同时为了尽量的节省栈空间,局部变量表的Slot是能够重用的。但这样也会形成一个反作用,会直接影响到系统的垃圾收集行为。blog
操做数栈也常称为操做栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表同样,操做数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。继承
操做数栈的每个元素能够是任意的Java数据类型,包括long和double。32位的数据类型所占的栈容量为1,64位的数据类型所占的栈容量为2。在方法执行的任什么时候候,操做数栈的深度都不会超过max_stacks数据项中设定的最大值。
当一个方法刚刚开始执行的时候,这个方法的操做数栈是空的,在方法的执行过程当中,会有各类字节码指令往操做数栈中写入和提取内容,也就是出栈/入栈操做。
每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接。
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用做为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析;另一部分将在每一次运行期间转化为直接引用,这部分称为动态链接。
当一个方法开始执行后,只有两种方式能够退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。
另一种退出方式是,在方法执行过程当中遇到了异常,而且这个异常没有在方法体内获得处理,不管是Java虚拟机内部产生的异常,仍是代码中使用throw字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会致使方法退出,这种退出方法的方式称为异常完成出口。一个方法使用异常完成出口的方式退出,是不会给它的上层调用者产生任何返回值的。
不管采用何种退出方式,在方法退出以后,都须要返回到方法被调用的位置,这样程序才能继续执行。通常来讲,方法正常退出时,调用者的PC计数器的值能够做为返回地址,栈帧中极可能会保存这个计数器值。而方法异常退出时,返回地址是要经过异常处理器表来肯定的,栈帧中通常不会保存这部分的信息。
方法退出的过程实际上就等同于把当前栈帧出栈,所以退出时可能执行的操做有:恢复上层方法的局部变量表和操做数栈,把返回值(若是有的话)压入调用者栈帧的操做数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
虚拟机规范容许具体的虚拟机实现增长一些规范中没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息彻底取决于具体的虚拟机实现。
在实际的开发中,通常会把动态链接、方法返回地址和其余附加信息所有归为一类,称为栈帧信息。
方法调用并不等同于方法执行,方法调用阶段惟一的任务就是肯定被调用方法的版本(即调用哪个方法),暂时还不涉及方法内部的具体运行过程。
全部方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行以前就有一个可肯定的调用版本,而且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须肯定下来。这类方法的调用称为解析。
在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特色决定了它们都不可能经过继承或别的方式重写其余版本,所以它们都适合在类加载阶段进行解析。
与之相对应的是,在Java虚拟机里面提供了5条方法调用字节码指令,分别以下:
❤ invokestatic:调用静态方法;
❤ invokespecial:调用实例构造器<init>方法,私有方法和父类方法;
❤ invokevirtual:调用全部的虚方法:
❤ invokeinterface:调用接口方法,会在运行时再肯定一个实现此接口的对象;
❤ invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,而后再执行该方法,在此以前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
只要能被invokestatic和invokespecial指令调用的方法,均可以在解析阶段中肯定惟一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法4类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法能够称为非虚方法,与之相反,其余方法称为虚方法(除去final方法)。
Java中的非虚方法除了使用invokestatic、invokespecial调用的方法以外还有一种,就是被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的,可是因为它没法被覆盖,没有其余版本,因此也无需对方法接收者进行多态选择,又或者说多态选择的结果确定是惟一的。因此,final方法是一种非虚方法。
解析调用必定是个静态的过程,在编译期间就彻底肯定,在类加载的解析阶段就会把涉及的符号引用所有转变为可肯定的直接引用,不会延迟到运行期再去完成。
上面的全部内容就是虚拟机如何调用方法的内容。
参考:《深刻理解Java虚拟机》 周志明 编著: