【JVM】虚拟机字节码执行引擎

运行时栈结构

每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。在编译时,栈帧中需要多大的局部变量表,需要多深的操作数栈就已经被分析出来了。
以Java程序执行的角度看,同一时刻、同一条线程里,在调用堆栈的所有方法同时处于执行状态。而对于执行引擎来说,只有当前栈帧,这个栈帧所对应的当前方法才是生效的。

在这里插入图片描述

局部变量表

局部变量表用于存放方法参数和方法内部定义的局部变量。当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果执行的是实例方法(没有被static修饰),则局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用。
为了节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的,如果当前PC计数器的值已经超出了某个变量的作用域,那这个变量对应的变量槽可以交给其他变量来用。
局部变量定义了但没有赋初始值,则不能使用

操作数栈

操作数栈具有先入后出的特点,当一个方法刚开始执行的时候,这个方法的操作数栈为空,在这个方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容。例如,整数加法指令iadd,这条指令要求在运行时最接近栈顶的两个元素已经存入了两个int值,这两个int值会出栈相加再将相加的结果重新入栈。
操作数栈中元素的数据类型必须与字节码指令的序列严格配合。用iadd指令时,只能用于两个int相加,不能一个long一个float
在概念模型中,两个不同的栈帧作为不同方法的虚拟机栈元素,应该相互独立的,但实际上下面虚拟机栈的操作数栈和上面栈帧的局部变量表有一部分重合。这样做不仅节约了一部分空间,还能在方法调用时直接共用一部分数据,无需进行额外的参数传递
在这里插入图片描述

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,在类加载过程中,有一部分符号引用会解析为直接引用,但另一部分会在每次运行期间都转化为直接引用,这一部分被称为动态连接

方法返回地址

方法开始执行后,只有两种方式推出这个方法。第一种,执行引擎遇到任何一个方法返回的字节码指令,这个时候可能会有返回值传递给上层的方法调用者,方法是否有返回值及返回值的类型将根据遇到何种方法返回指令决定,这种退出方法被称为“正常调用完成”
另一种退出方式在方法执行的过程中遇到异常,并且这个异常没有在方法体内得到妥善处理。
无论何种退出方式,在方法退出后,都必须返回最初方法被调用时的位置,程序才能继续执行,方法正常退出时,主调方法的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值,方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中不会保存这部分信息
方法的退出过程就是出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令的后一条指令。

方法调用

解析

调用不同类型的方法,字节码指令集里设计了不同指令。

  1. invokestatic 用于调用静态方法
  2. invokespecial 用于调用实例构造器()方法、私有方法和父类中的方法
  3. invokevirtual 用于调用所有的虚方法
  4. invokeinterface 用于调用接口方法,会在运行时再确定一个实现接口的对象
  5. invokedynamic 先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法
    只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法
    再加上被final修饰的方法(尽管使用invokevirtual指令调用),这5钟方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用,这些方法被称为非虚方法,反之其他为虚方法。

分派

1.静态分派
根据静态类型来决定方法执行版本的分派动作,被称为静态分派,静态分派一般发生在编译阶段。
虚拟机在重载时通过参数的静态类型而不是实际类型作为判断依据,由于静态类型在编译器可知,所以在编译阶段,编译器就可以根据参数的静态类型决定会使用哪个版本的重载
在这里插入图片描述
‘a’是一个char类型数据,先寻找char的重载方法,注释掉sayHello方法,则会将‘a’代表数字97(Unicode数值为97),再注释掉int,进一步转型为long,继续注释,则将它封装为类型Charcter,注释掉Charcter后,因为java.lang.Serializable是java.lang.Character实现的一个接口,继续注释,则输出为hello,Object,这是char装箱后转型为父类,最后才是char[]数组。

2.动态分派

Invoke指令

  1. 找到栈顶元素所引用过的对象的实际数据,记为C
  2. 在C中找到与常量中的描述符与简单名称符合的方法,再进行访问权限校验,没问题后返回这个方法的直接引用
  3. 否则,就按继承关系从下往上一次对C的各个父类进行第二部搜索和验证,若找到与常量中简单名称和描述都都符合的方法,则返回·方法的直接引用
  4. 否则,抛出异常
    我们把这种在运行期间根据实际类型确定方法执行版本的分派过程称为动态分派

字段永远不参与多态,哪个类的方法访问某个名字的字段时,该名字指的是这个类的那个字段。子类声明与父类同名的字段时,虽然在子类的内存中有两个字段存在,但子类的字段会遮蔽父类的同名字段。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在Son类在创建的时候,首先隐式调用了Father的构造函数,Father构造函数中对showmethemoney()调用是一次虚方法调用,所以执行的是son的版本,这时父类中的money被初始化为2,但此时调用的是子类的字段,son没初始化完成,所以为0,之后子类进行初始化,输出为4,最后为编译期就确定的静态类型访问父类的money,所以为2
3. 单分派与多分派
根据一个宗量对目标方法进行选择称为单分派,根据多于一个宗量对目标方法进行选择称为多分派
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
编译阶段只有一个类型Father对应了hardChoice(new_360())和hardChoice(new QQ ()),多于一个宗量对目标方法进行分配,所以是多分派
运行阶段执行son.hardChoice(new QQ())这行代码时,编译器已经决定目标方法签名必须为hardChoice(new QQ()),这时方法的静态类型,实际类型都对方法选择不会构成任何影响,唯一可以影响虚拟机选择的只有是father还是son,因为只有一个宗量作为选择依据,所以为单分派
Java是一门静态多分派,动态单分派的语言

4.虚拟机动态分派的实现
因为动态分派方法版本选择过程需要运行时在接收者类型的方法元数据中搜索合适的目标方法,所以在方法区中建立了虚方法表,用虚方法表索引来代替元数据查找以提高性能
虚方法表中存放着各个方法的实际入口,如果某个方法在子类中中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址是一致的,都指向父类的实现入口,如果子类中重写了这个方法,子类虚方法表中的地址会被替换为指向子类实现版本的入口地址
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值