持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天,点击查看活动详情
字节码执行引擎的作用
字节码执行引擎是字节码文件执行的一种概念模型,将输入的字节码文件进行解析处理来得到程序的执行结果。与方法调用、方法执行息息相关。
运行时栈帧结构
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
局部变量表
- 定义:局部变量表是一组变量值存储空间
- 作用:存放方法参数和方法内部定义的局部变量
- 分配时期:
Java程序编译为Class文件时,会在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量
- 最小单位:变量槽
-
大小:虚拟机规范中没有明确指明一个变量槽占用的内存空间大小,允许变量槽长度随着处理器、操作系统或虚拟机的不同而发生变化
-
-
对于
32位以内的数据类型(boolean、byte、char、short、int、float、reference、returnAddress),虚拟机会为其分配一个变量槽空间
-
-
-
对于
64位的数据类型(long、double),虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间
-
-
特点:可重用。为了尽可能节省栈帧空间,若当前字节码
PC计数器的值已超出了某个变量的作用域,则该变量对应的变量槽可交给其他变量使用
- 访问方式:通过索引定位。索引值的范围是从 0 开始至局部变量表最大的变量槽数量
- 局部变量表第一项是名为
this的一个当前类引用,它指向堆中当前对象的引用(由反编译得到的局部变量表可知)
操作数栈
- 定义:操作数栈是一个后入先出栈
- 作用:在方法执行过程中,写入(进栈)和提取(出栈)各种字节码指令
- 分配时期:同上,在编译时会在方法的
Code属性的max_stacks数据项中确定操作数栈的最大深度
- 栈容量:操作数栈的每一个元素可以是任意的
Java数据类型 ——32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2
注意:操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译时编译器需要验证一次、在类校验阶段的数据流分析中还要再次验证
动态链接
- 定义:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
- 静态解析和动态连接区别:
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,这些符号引用:
-
一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态解析)
-
另一部分会在每一次运行期间转化为直接引用(动态连接)
方法返回地址
- 方法退出的两种方式:
-
正常退出:执行中遇到任意一个方法返回的字节码指令
-
异常退出:执行中遇到异常、且在本方法的异常表中没有搜索到匹配的异常处理器区处理
- 作用:在方法返回时都可能在栈帧中保存一些信息,用于恢复上层方法调用者的执行状态
-
正常退出时,调用者的
PC计数器的值可以作为返回地址
-
异常退出时,通过异常处理器表来确定返回地址
- 方法退出的执行操作:
-
恢复上层方法的局部变量表和操作数栈
-
若有返回值把它压入调用者栈帧的操作数栈中
-
调整
PC计数器的值以指向方法调用指令后面的一条指令等
方法调用
方法调用即指确认调用哪个方法的过程,并不是指执行方法的过程。Java 的编译并不包含传统编译过程中的连接步骤,所以在 .java 代码编译成 .class 文件之后,在 .class 文件中存储的是方法的符号引用(方法在常量池中的符号),并不是方法的直接引用(方法在内存布局中的入口地址),所以需要在加载或运行阶段才会确认目标方法的直接引用。
解析调用
有几种方法的调用,在加载阶段就可以确认该方法的直接引用,前提是:方法在程序真正运行之前就有一个可确定的调用版本(调用哪一个方法),并且这个方法的调用版本在运行期是不可变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析。
有四种方法是进行的方法的解析:静态方法、私有方法、实例构造器、父类方法,这四类方法称为非虚方法,与之对应的就是续方法(final 方法除外),调用这四类方法的字节码指令是:invokestatic、invokespecial 指令,也就是说被 invokestatic、invokespecial 字节码调用的方法,在类加载的解析阶段就可以通过方法的符号引用确认方法的直接引用。在 Java 字节码中,还有几种调用方法的字节码指令如下:
- invokestatic:调用静态方法
- invokespecial:调用实例构造器方法、私有方法、父类方法
- invokevirtual:调用所有的虚方法
- invokeinterface:调用接口方法,会在运行时确认一个实现此接口的对象
- invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。被 final 关键字修饰的方法,在字节码中是被 invokevirtual 指令调用的,但是被 final 修饰的方法无法被重载或重写,所以只有一个方法,在加载阶段就可以确认调用哪个方法,所以也是一种虚方法,方法调用时走的也是解析流程。
分派调用
解析调用是一个静态的过程,在加载阶段就可以确认目标方法的直接引用。分派调用有可能是静态的,也有可能是动态的,根据分派的宗量数又可以分为单分派和多分派,这两类两两组合,所以分派共可以细分为:静态单分派、静态多分派、动态单分派、动态多分派。
在讲解本节中的分派的过程中,会揭示一些 Java 中的多态性在 Java 虚拟机层面的基本体现,如“重载”和“重写”在 Java 虚拟机中是如何实现的。
静态分配
- 依赖静态类型来定位方法的执行版本
- 典型应用是方法重载
- 发生在编译阶段,不由
JVM来执行
动态分派
- 依赖动态类型来定位方法的执行版本
- 典型应用是方法重写
- 发生在运行阶段,由
JVM来执行
单分派
根据一个宗量对目标方法进行选择(方法的接受者与方法的参数统称为方法的宗量)
多分派
根据多于一个宗量对目标方法进行选择