虚拟机字节码执行引擎

运行时栈帧结构

栈帧(Stack Frame)是用于虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程,都对应着一个栈帧从入栈到出栈的过程。

虚拟机字节码执行引擎_第1张图片
栈帧的结构

1、局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法需要分配的局部变量表的最大容量。

在方法执行时,虚拟机是使用局部变量表完成参数值到参数列表的传递过程,如果执行的是实例方法(非static方法),那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完成后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。

为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。某些情况下,Slot复用会影响到垃圾收集行为。

2、操作数栈

操作数栈(Operand Stack)也称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。

在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。

虚拟机字节码执行引擎_第2张图片
两个栈帧之间的数据共享

在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。但在大多数虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的局部变量表重叠到一起,这样在进行方法调用时就可以共用一部分数据,无需进行额外的参数复制传递。

3、动态连接

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

4、方法返回地址

当一个方法开始执行后,只有两种方式可以退出这个方法。

1、执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。

2、另一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常方法出口。

5、附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到虚拟机栈帧之中。

方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本,暂时还不涉及方法内部的具体运行过程。

1、解析

所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期间不会改变。

在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。

与之对应,Java虚拟机里提供了5条方法调用字节码指令。

invokestatic:调用静态方法

invokespecial:调动实例构造器方法、私有方法和父类方法

invokevirtual:调用所有的虚方法

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

invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

2、分派

Java是一门面向对象的程序语言,因为Java具备面向对象的3个基本特征:继承、封装和多态。分派调用将会揭示多态特征的一些最基本体现,如“重载”和“重写”在Java虚拟机中是如何实现的。

2.1、静态分派

重载

2.2、动态分派

invokevirtual指令运行时解析过程大致分为以下步骤:

1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。

2、如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。

3、否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。

4、如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

由于invokevirtual指令执行的第一步就是在运行期确定接受者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言中方法重写的本质。我们把运行期间根据实际类型确定方法执行版本的过程称为动态分派。

2.3、单分派与多分派


2.4、虚拟机动态分派的实现


3、动态类型语言支持

3.1、动态类型语言


3.2、JDK1.7与动态类型


3.3、java.lang.invoke包


3.4、invokedynamic指令


3.5、掌握方法分派规则


4、基于栈的字节码解释执行引擎


4.1、解释执行

虚拟机字节码执行引擎_第3张图片
编译过程

4.2、基于栈的指令集与基于寄存器的指令集


4.3、基于栈的解释器执行过程


摘自 《深入理解Java虚拟机》

你可能感兴趣的:(虚拟机字节码执行引擎)