在上一篇我们介绍了类加载器的相关功能,在这一篇中我们在分享一下虚拟机中的另一个非常重要的功能字节码执行引擎。我们知道Java虚拟机的主要任务就是加载class文件并执行其中的字节码。加载class的功能是由类加载器实现的,那么执行其中字节码的功能就是由字节码执行引擎执行的。下图为虚拟机的基本结构图。
虚拟机的执行引擎有很多种,不同的执行引擎也有很大的差别,它们主要的区别如下:
上面所说的都是不同执行引擎的的执行特性,但无论是哪一种执行引擎其中最基本的功能都是执行字节码。下面我们来看一下在执行引擎内部是怎么保证字节码的正确执行的。
栈帧是虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、 操作数栈、 动态连接和方法返回地址等信息。每一个方法从调用开始到执行结束,在虚拟机内部的过程就是栈帧在栈里面从入栈到出栈的过程。在虚拟机中栈是针对线程来操作的,也就是说每一个线程都有一个独立的栈内存。如果线程中的方法调用链很长,并且很多方法都同时在执行。那么虚拟机怎么保证方法的正常调用呢?
其实对于执行引擎来说,在活动的线程中,只有位于栈顶的栈帧才是有效的,它被称之为当前栈帧,和这个栈帧相关联的方法称为当前方法。 执行引擎运行时的字节码指令都是针对当前栈帧进行操作的。下图,为栈帧的概念图。
下面我们重点来分享一下栈帧中的局部变量表、 操作数栈、 动态连接、 方法返回地址等各个部分的作用和数据结构。
局部变量表是存储变量的内存空间,主要存储的是方法参数和方法内部定义的局部变量。虚拟机使用索引的方式访问局部变量表中的变量,并且索引是从0开始的。在方法执行时,虚拟机使用局部变量表完成参数值到参数变量列表的传递过程。如果执行的是实例方法(非静态方法),那么局部变量表中的第0个索引默认存储的是方法所属对象的实例,说白点也就是我们开发时所用的this关键字。
操作数栈是一个后进先出的栈。操作数栈可以存储任意的Java数据类型。当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。 下面我们看一下如果在Java中执行两个int类型数据的相加,那么操作数栈是怎么处理的。首先需要将两个int类型的数据执行入栈操作,并且保证,这两个int类型数据必须最接近栈顶。当执行这个相加指令时,会将这两个int类型数据进行出栈相加,然后将相加后的结果在执行入栈操作。操作数栈中的数据类型必须与字节码指令匹配,就像上述说明中的相加操作,因为我们执行的是int类型的相加操作,所以在执行时,最接近栈顶的两个数据类型必须是int类型,不能出现一个long和一个float相加的情况。在编译器执行和类加载时,都会进行上述的校验操作。
在正常的情况下操作数栈与操作数栈都是互相独立的。但在虚拟机优化时通常会做一些处理也就是两个操作数栈会有重叠的部分,也就是方法调用时这一部分数据会被共享。目的是减少额外的参数传递和复制等操作。
每个栈帧都有一个指向运行时常量池该栈帧所属方法的引用,持有这个引用的目的是为了支持方法调用过程中的动态连接。 我们知道class文件的常量池中存有大量的符号引用,字节码中的方法调用就以常量池中指向方法的符号引用作为参数。 这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。 另外一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
当一个方法执行后,只有2种办法可以让虚拟机退出这个方法。它们分别是:
方法退出的实际过程就是把当前栈帧出栈。方法退出所执行的具体逻辑是:恢复一个方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。