深入理解Java虚拟机:字节码执行引擎

一、运行时栈帧结构

栈帧:用于支持虚拟机进行方法调用和方法执行的数据结构,也是虚拟机运行时数据区中的虚拟机栈的栈元素。

栈帧存储了方法的局部变量表、操作数栈、动态链接、方法返回地址。

在活动线程中,只有位于栈顶的方法才是运行的,只有位于栈顶的栈帧才是有效的。

1)局部变量表

一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。

变量槽(Slot)为最小单位

boolean、byte、char、short、int、float、reference、returnAddress分别占用一个变量槽。

long、double分别占用两个变量槽。

当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。

当执行的是非static修饰的方法时,布局变量表中第0位索引的变量槽默认为传递方法所属对象实例的应用。

为了节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的。

2)操作数栈

一个后进先出的栈。

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

优化处理:令两个栈帧出现一部分重叠;节约空间、在记性方法调用时可以直接公用一部分数据,无须进行额外的参数赋值传递了。

3)动态链接

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

4)方法返回地址

当一个方法开始执行后,只有两种方式能退出该方法。

正常调用完成:执行引擎遇到任意一个方法返回的字节码指令。

异常调用完成:在方法执行的过程中遇到了异常,并且没有catch或throw的异常。

二、方法调用

1)解析

调用不同类型方法的指令

invokestatic:调用静态方法。

invokespecial:调用实例构造器()方法、私有方法和父类的方法。

invokevirtual:调用所有的虚方法。

invokeinterface:调用接口方法。

invokedynamic:运行时动态解析出调用点限定符引用的方法,在执行该方法。

方法调用方式:静态(在编译期间就完全确定,在类加载的解析阶段就会把符号引用转变为直接引用);分派

2)分派

静态分派

所有依赖静态类型来决定方法执行版本的分派动作。

典型应用表现:方法重载

动态分派

典型应用表现:方法重写

单分派与多分派

虚拟机动态分派的实现

invokevirtual运行时解析过程

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

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

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

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

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

1)基于栈的指令集与基于寄存器的指令集

基于栈的指令集:Javac编译器输出的字节码指令流,字节码指令流里面的指令大部分都是零地址指令,它们依赖操作数栈进行工作。

举例说明:

iconst_1

iconst_1

iadd

istore_0

基于寄存器的指令集:主流PC机中物理硬件直接支持的指令集架构,依赖寄存器进行工作。

举例说明:

mov eax, 1

add eax, 1

特点:

基于栈的指令集可移植,基于寄存器的指令集由硬件控制,受到硬件的约束;

基于栈的指令集代码相对更加紧凑(字节码中每个字节就对应一条指令,而多地址指令集中还需要存放参数),但是完成相同功能所需的指令数量也会多。

编译器实现更加简单(不需要考虑空间分配的问题,所需空间都在栈上操作)

基于栈的指令集执行速度相对慢

基于栈的指令集实现在内存中,频繁的内存访问,是执行速度的瓶颈

2)基于栈的解释器执行过程

执行偏移地址为0的指令,bipush指令的作用是将单字节的整形常量值(-128~127)推入操作数栈顶,跟随一个参数,指明推送的常量值100。

执行偏移地址为2的指令,istore_1指令的作用是将操作数栈顶的整型值出栈并存放在第1个局部变量槽中。同理,偏移量地址为3,4,7,10的指令操作。

执行偏移地址为11的指令,iload_1指令的作用是将局部变量表第1个变量槽中的整形值复制到操作数栈顶。

执行偏移地址为12的指令,iload_2指令的执行过程与iload_1类似,把第2个变量槽的整型值入栈。

执行偏移地址为13的指令,iadd指令的作用是将操作数栈中头两个栈顶元素出栈,做整形假发,然后把结果重新入栈。在iadd指令执行完毕后,栈中原有的100和200被出栈,它们的和300被重新入栈。

执行偏移地址为14的指令,iload_3指令把存放在第3个局部变量槽中的300入栈到操作数栈中。这时操作数栈为两个整数300,。下一条指令imul是将操作数栈中头两个栈顶元素出栈,做整形乘法,然后把结果重新入栈,与iadd一样。

执行偏移地址为16的指令,ireturn指令是方法返回指令之一,它将结束方法执行并将操作数栈顶的整型值返回给该方法的调用者。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20200504A0OZEW00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券