Java虚拟机进阶之路——虚拟机字节码执行引擎

概述

执行引擎是Java虚拟机核心组成部分之一,它在执行字节码时有两种方式:解释执行和编译执行(通过即时编译器产生本地代码执行),统一输入二进制字节码流,输出执行结果。

运行时栈帧结构

Java虚拟机以方法做为基本执行单元,栈帧(stack Frame)是用于支持虚拟机进行方法调用和方法执行背后的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等。

对于执行引擎来说,只有位于栈顶的方法才是在运行的,只有栈顶的方法才是生效的,其被称为当前栈帧,与这个栈帧所关联的方法被称为“当前方法”。

局部变量表

  是一种变量值的存放空间,作用是存放方法参数方法定义的局部变量

局部变量表以变量槽(variable slot)为最小单位。一个变量槽可以存放一个32位以内的数据类型,Java中占用不超过32位存储空间的数据类型有booleanbytecharshortintfloatreference[1]returnAddress8种类型。前面6种不需要多加解释,读者可以按照Java语言中对应数据类型的概念去理解它们(仅是这样理解而已,Java语言和Java虚拟机中的基本数据类型是存在本质差别的),而第7reference类型表示对一个对象实例的引用,《Java虚拟机规范》既没有说明它的长度,也没有明确指出这种引用应有怎样的结构。但是一般来说,虚拟机实现至少都应当能通过这个引用做到两件事情,一是从根据引用直接或间接地查找到对象在Java堆中的数据存放的起始地址或索引,二是根据引用直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息。

局部变量槽存放不大于32位的数据,若大于,可以分割成两次32位的读写操作。对于两个相邻的共同存放一个64位数据的变量槽,虚拟机不允许单独访问其中任何一个,保证数据原子性。

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

为了节省空间,变量槽是可以复用的。

关于局部变量表,还有一点可能会对实际开发产生影响,就是局部变量不像前面介绍的类变量那

样存在准备阶段。通过第7章的学习,我们已经知道类的字段变量有两次赋初始值的过程,一次在准

备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的初始值。因此即使在初始化阶

段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值,不会产生歧义。但局部

变量就不一样了,如果一个局部变量定义了但没有赋初始值,那它是完全不能使用的。

Eg:public static void main(String[] args) {

int a;

System.out.println(a);

}

这里的a就不能用。

操作数栈

也称操作栈,后入先出,是方法执行过程中的中间结果。

当一个方法开始执行的时候,这个方法的操作数栈是空的,在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,对应入栈和出栈。

操作数栈中元素的数据类型必须和字节码指令顺序严格匹配。以上面的iadd指令为例,这个指令只能用于整型数的加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况。

另外在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在

大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠。目的:①节约空间②在方法调用时可以直接共用部分数据。

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的符号引用,持有这个引用是为了调用过程中的动态连接。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。

方法返回地址

方法开始执行后,有两种方式退出这个方法,第一种是执行引擎遇到任意一个方法返回的字节码指令,这种称为“正常调用完成”。

另一种是遇到异常,并且异常在方法体中没有得到妥善处理,称为“异常调用完成”。

无论哪种退出方式,在方法退出后,都必须返回到最初方法被调用时的位置,程序才能继续执行。

方法调用

方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本

(即调用哪一个方法)。

解析

承接前面关于方法调用的话题,所有方法调用的目标方法在Class文件里面都是一个常量池中的符

号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能够成立的前

提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不

可改变的。换句话说,调用目标在程序代码写好、编译器进行编译那一刻就已经确定下来。这类方法

的调用被称为解析(Resolution)。

调用不同类型的方法,字节码指令集里设计了不同的指令。在Java虚拟机支持以下5条方法调用字

节码指令,分别是:

·invokestatic。用于调用静态方法。

·invokespecial。用于调用实例构造器<init>()方法、私有方法和父类中的方法。

·invokevirtual。用于调用所有的虚方法。

·invokeinterface。用于调用接口方法,会在运行时再确定一个实现该接口的对象。

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

只要能被invokestaticinvokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,

Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上final修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。这些方法统称为“非虚方法Non-Virtual Method),与之相反,其他方法就被称为“虚方法Virtual Method)。

解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号

引用全部转变为明确的直接引用,不必延迟到运行期再去完成。

   分派(Dispatch调用则要复杂许多,它可能是静态的也可能是动态的,按照分派依据的宗量数可分为单分派和多分派[1]。这两类分派方式两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。

静态分派依赖方法静态类型来执行方法版本的分派动作。对应方法重载(非常重要)

虚拟机(或者准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。由于静态类型在编译期可知,所以在编译阶段,Javac编译器就根据参数的静态类型决定了会使用哪个重载版本。

动态分派:与对象实际类型相关,体现在重写(与前面对应)

本质是根据方法接收者的实际类型来确定方法版本。

另外注意字段不参与多态,不具备多态性。

静态分派是多分派,动态分派是单分派

/**

* 单分派、多分派演示

* @author zzm

*/

public class Dispatch {

static class QQ {}

static class _360 {}

public static class Father {

public void hardChoice(QQ arg) {

System.out.println("father choose qq");

}

public void hardChoice(_360 arg) {

System.out.println("father choose 360");

}

}public static class Son extends Father {

public void hardChoice(QQ arg) {

System.out.println("son choose qq");

}

public void hardChoice(_360 arg) {

System.out.println("son choose 360");

}

}

public static void main(String[] args) {

Father father = new Father();

Father son = new Son();

father.hardChoice(new _360());

son.hardChoice(new QQ());

}

}

运行结果:

father choose 360

son choose qq

main()里调用了两次hardChoice()方法,这两次hardChoice()方法的选择结果在程序输出中已经显示得很清楚了。我们关注的首先是编译阶段中编译器的选择过程,也就是静态分派的过程。这时候选择目标方法的依据有两点:一是静态类型是Father还是Son,二是方法参数是QQ还是360这次选择结果的最终产物是产生了两条invokevirtual指令,两条指令的参数分别为常量池中指向Father::hardChoice(360)Father::hardChoice(QQ)方法的符号引用。因为是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型

再看看运行阶段中虚拟机的选择,也就是动态分派的过程。在执行“son.hardChoice(new QQ())”

行代码时,更准确地说,是在执行这行代码所对应的invokevirtual指令时,由于编译期已经决定目标方法的签名必须为hardChoice(QQ),虚拟机此时不会关心传递过来的参数“QQ”到底是腾讯QQ”还是奇瑞QQ”,因为这时候参数的静态类型、实际类型都对方法的选择不会构成任何影响,唯一可以影响虚拟机选择的因素只有该方法的接受者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,

所以Java语言的动态分派属于单分派类型

虚拟机动态分派的实现:

动态分派是执行非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在接收者类型的

方法元数据中搜索合适的目标方法,因此,Java虚拟机实现基于执行性能的考虑,真正运行时一般不会如此频繁地去反复搜索类型元数据。面对这种情况,一种基础而且常见的优化手段是为类型在方法区中建立一个虚方法表(Virtual Method Table,也称为vtable,与此对应的,在invokeinterface执行时也会用到接口方法表——Interface Method Table,简称itable),使用虚方法表索引来代替元数据查找以提高性能。

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方

法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址。

  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值