JVM深入浅出(9)--- 虚拟机字节码执行引擎

1. 概述

物理机的执行引擎是直接建立在操作系统,处理器,缓存,指令集上。而虚拟机的执行引擎是软件实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系

在不同虚拟机中,执行引擎在执行字节码的时候,通常有解释执行(通过解释器执行)编译执行(即通过编译器产生本地代码执行),也可能两者兼备。

所有执行引擎的输入输出都是一样的,输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果

解释器是通过将字节码指令转换成对应平台的机器指令后执行。

JVM 中的编译器特指 JIT(Just-In-Time)编译器,是 HotSpot 虚拟机的核心优化组件。JIT 编译器不是 “一次性编译所有字节码”,而是 “按需编译。JVM 运行时会统计代码的执行频率,当代码达到 “热点阈值”(默认一万次调用),标记为 “热点代码”。JIT 编译器在后台将热点字节码编译为与当前 CPU 架构匹配的机器码,并存入代码缓存

2. 运行时栈帧结构

栈帧是支持方法调用和方法执行的数据结构,栈帧存储了局部变量表,操作数栈,动态连接,方法返回地址等信息

栈帧的大小是在编译就已经决定好了,不会收到运行期变量的影响。

只有位于栈顶的栈帧是运行的,称为当前栈帧

2.1 局部变量表

局部变量表用来放方法参数和方法内部定义的局部变量。以变量槽为单位,一个变量槽可以放32位以内的数据类型,Java中占用不超过32位存储空间的数据类型有boolean、byte、char、short、int、 float、reference和returnAddress这8种类型。

对于64位的数据类型long,double,Java会使用两个连续的变量槽来存储。

局部变量表的访问是通过从0到N的索引来访问的,对于占用一个变量槽的元素,索引N 表示第N个槽。但是访问64位元素时,会要求同时使用N和N+ 1两个槽,不允许任何方式单独访问其中一个。局部变量表中的变量槽可以重⽤

如果方法是实例方法,局部变量表的0默认是this,局部变量表能完成形参到实参的传递。

还记得吗局部变量表的元素可以作为gcroots去做可达性算法,当局部变量脱离作用域后,局部变量表的变量可能还占用插槽,导致其不会被回收。

2.2 操作数栈

  • 操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项 之中

  • 操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占 的栈容量为1,64位数据类型所占的栈容量为2

  • 刚开始时,操作数栈是空的,在方法执行过程中,会有字节码指令往操作数栈中写入和提取内容

  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配

  • 虚拟机中会做内存优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数 栈与上面栈帧的部分局部变量表重叠在一起,共用一部分数据。

image-20260213220649816

2.3 动态连接

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

  • 在类加载期间的符号引用转化为直接引用,这种叫做静态连接
  • 在运行期间的符号引用转化为直接引用,叫做动态连接。

2.4 方法返回地址

方法的退出有两种方式

  • 正常调用完成 :执行引擎遇到方法返回的字节码指令,这时候会把返回值传递给上层调用者
  • 异常调用完成:在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理

无论是哪种退出,程序都需要回到调用前的状态,所以方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态

2.5 附加信息

  • 《Java虚拟机规范》允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、 性能收集相关的信息,这部分信息完全取决于具体的虚拟机实现
  • 在讨论概念时,一 般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息

3.方法调用

方法调用不等于方法被执行,方法调用阶段的唯一任务就是确定调用方法的版本(即确认调用哪一个方法)

3.1 解析

解析成立的前提:在符号引用转为直接引用时,真正运行之前找到一个确定的调用版本,且这个版本在运行期是不可变的

编译器可知,运行期不可变的方法:静态方法&私有方法,这种特性表明他们都适合在加载时解析

调用不同方法的指令

  • invokestatic:调用静态方法
  • invokespecial:调用<init>,私有方法和父类的方法
  • invokevirtual:调用所有虚方法
  • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法
  • invokeinterface:用于调用接口方法,

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本, Java语言里符合这个条件的方法共有静态方法、私有方法、实例构造器、父类方法4种,再加上被final 修饰(虽然这个是用invokevirtual调用的)的方法,这五种方法会在类加载时就解析,称之为非虚方法,而其他方法称为虚方法

能在类加载阶段的解析是静态解析

3.2 分派(分派调用,也就是如何确定调用方法的版本)

分派 (Dispatch)调用可能是静态的也可能是动态的,按照分派依据的宗量数可分为单 分派和多分派

首先什么是静态类型和实际类型?

  • 静态类型就是变量的类型,如Human man = new Man();,Human 就是静态类型
  • 实际类型就是实际分配对象的类型,如上面 Man就是实际类型
  • 静态类型和实际类型在程序中都可能发生变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类 型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么

四种分派:

  • 静态分派

    • 所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。例如重载,是根据静态类型来决定方法的调用,这是静态分派最好的例子,静态分派发生在编译器。需要注意 Javac 编译器虽然能确定出⽅法的重载版本,但在很多情况下这个 重载版本并不是 “ 唯⼀ ” 的,往往只能确定⼀个 “ 相对更合适的 ” 版本
  • 动态分派

    • 动态分派是用动态类型来决定方法执行的版本(其实也就是动态绑定),重写是最典型的例子。

    • 在字节码指令中,调用invokevirtual虽然都指向了同样的符号引用,但结果却不同。invokevirtual指令的运行时解析过程如下

      • 找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
      • 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果 通过则返回这个方法的直接引用,查找过程结束;不通过则返回java.lang.IllegalAccessError异常。
      • 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程
      • 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常

      说白了,正是因为invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以调用方法并不是找到符号引用就结束了。

  • 单分派与多分派

    • 方法的接收者与方法的参数统称为方法的宗量单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。
    • Java语言的静态分派属于多分派类型,动态分派属于单分派类型。

虚拟机动态分派的实现

由于动态分派需要运行时在接收者类型的 方法元数据中搜索合适的目标方法,基于性能考虑,虚拟机不会多次去查询元数据,而是建立一个虚方法表。使用虚方法表索引来代替元数据查找以 提高性能

image-20260214011844640

posted @ 2026-04-07 17:55  不会coding的喵酱  阅读(0)  评论(0)    收藏  举报