JVM
字节码执行引擎的功能基本就是:
JVM
字节码执行引擎的实现方式可以是:
参考 《认识
JVM
规范》 中的 栈帧
栈帧的说明:
栈帧是用于支持 JVM
进行方法调用和方法执行的数据结构;
栈帧随着方法调用而创建,随着方法结束而销毁;
栈帧里面存储了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息。
栈帧的概念结构如下图所示:
参考 《认识
JVM
规范》 中的 局部变量表
局部变量表的说明:
用来存放方法参数和方法内部定义的局部变量的存储空间;
以变量槽 slot
为单位,一个 slot 存放 32
位以内的数据类型(如 byte
、char
、short
、int
、float
、引用类型);
对于 64
位的数据占 2 slot
(如 long
、double
);
对于实例方法,索引 0 中的 slot
存放的是 this
,然后从索引 1
到 n
,依次分配给参数列表以及局部变量;
局部变量所占的 slot
是根据在方法体中局部变量的定义顺序以及作用域来分配的;
slot
是可复用的,以节省栈帧的空间。(这种设计可能会影响到系统的垃圾收集行为)
slot
是可复用的参考 《认识
JVM
规范》 中的Code
属性在class
文件中的解析举例(Slot
可复用)
操作数栈的说明:
用来存放方法运行期间,各个指令操作的数据。
操作数栈和局部变量表通过一些指针进行数据的存取,如:
iload_1
表示将局部变量表中的索引1
处的int
型数据取出,加载到操作数栈中。
istore_2
表示将操作数栈栈顶的int
型数据弹出,存储到局部变量表中索引2
处。
操作数栈中元素的数据类型必须和字节码指令的顺序严格匹配。
也就是说,对于
iload_1
指令,从局部变量表中取出的数据必须得是int
型,才能加载到操作数栈中。
虚拟机在实现栈中的时候可能会做一些优化,如:让两个栈帧出现部分重叠区域,以存放公用的数据。
每个栈帧会持有一个指向运行时常量池中该栈帧所属方法的引用,以支持方法调用过程的动态连接。
动态连接的两种方式:
静态解析:类加载的时候,符号引用转化为直接引用;
动态连接:运行期间,符号引用转化为直接引用。
方法返回地址:方法执行后返回的地址。
如方法
A
在方法B
中的位置P
被调用,那么方法A
的返回地址就是指向方法B
中的位置P
。
方法调用就是确定具体调用哪一个方法,并不涉及方法内部的执行过程。
也就是说方法调用的过程只是查找调用哪个方法的过程,而不包括方法体代码的执行过程。
方法调用的说明:
部分方法是直接在类连接的解析阶段就确定了直接引用关系;
但是对于实例方法,也称虚方法,因为重载和多态,需要运行期间动态分派。
实例方法也称虚方法,所以调用实例方法的指令是
invokevirtual
。
分派可分为静态分派和动态分派:
静态分派:所有依赖静态类型来定位方法执行版本的分派方式,比如:重载方法。
所谓的 “定位方法执行版本” 就是说定位具体调用哪一个方法。
动态分派:根据运行期的实际类型来定义方法执行版本,比如:覆盖方法(重写方法)。
另外,按照分派思考的维度,还可以分为单分派和多分派:
单分派:主需要单独考虑静态分派,或单独考虑动态分派。即在方法调用时只存在一种分派方法。
多分派:在方法调用时静态分派和动态分派都存在,如下代码所示:
main
函数中执行t.t1(5);
,调用t1
方法时:
首先需要考虑动态分派,确定变量
t
所引用对象的实际类型是Test2
还是Test3
;然后再考虑静态分派,根据传入的实参类型确定调用的是哪一个重载方法。
JVM
通过基于栈的字节码解释执行引擎来执行指令。JVM
的指令集也是基于栈的。
方法调用开始时创建栈帧,栈帧中保存了操作数栈和局部变量表。
方法调用执行的过程主要就是操作数栈和局部变量表之间的数据交互过程。
也就是说,操作数栈中的操作数是保存在局部变量表中的。