1 虚拟机字节码执行引擎
- 执行引擎是Java虚拟机核心的组成部分之一。
- "虚拟机"是一个相对于"物理机"的概念,这两种机器都有代码执行的能力,物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的。而虚拟机的执行引擎是由软件自行实现的。
- 本章会从*中制定的Java虚拟机字节码执行引擎的概念模型角度入手。
- 从外观上,所有的Java虚拟机的执行引擎输入、输出都是一致的。输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。
- 在不同的虚拟机实现里面,执行引擎在执行 Java 代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎。
2 运行时栈帧结构
- Java虚拟机以方法作为最基本的执行单元,"栈帧"则是用于支持虚拟机进行方法调用和方法执行背后的数据结构。它也是虚拟机运行时数据区中的虚拟机栈的栈元素。
- 每一个栈帧都包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。栈帧的内存取决于程序源码和具体的虚拟机实现的栈内存布局形式,不会受到程序运行期变量数据的影响。
2.1 局部变量表
- 是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。
- 局部变量表以变量槽为最小空间,*没有指定它的内存大小,只是很有导向性地说明每个变量槽应该存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。所以每个变量槽可以占用32位长度的内存空间,当然它允许变量槽的长度随着处理器、操作系统或虚拟机实现的不同而发生变化。
- Java虚拟机通过索引定位的方式使用局部变量表,索引值的范围从0开始至局部变量表的最大的变量槽数量。访问的是32位数据类型的变量,索引N就表示使用了第N个变量槽。访问的是64位数据类型的变量,说明同时会使用第N和第N+1两个变量槽。
- 局部变量表中的变量槽是可以重用的。方法体中定义的变量,它的作用域不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的变量槽就可以交给其他变量来重用。坏处:可能会影响到系统的垃圾收集行为。
- 下面的代码限制了placeholder的作用域,可是执行System.gc之后,它没有被回收掉。因为再出去之后,没有其他对局部变量表的读写操作,所以placeholder所占的变量槽还没有被其他变量复用,所以局部变量表仍然保持对它的关联。
public class LocalVariablesTableDemo {
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
//int a = 0 ;
System.gc();
}
}
- 局部变量表需要赋初始值。类的字段变量有两次赋值过程,一次在准备阶段,赋予系统初始值,一次在初始化阶段,赋予程序员定义的初始值。(因此即使没有为类变量赋值也没有关系,会有一个确定的初始值)。但是局部变量没有赋初始值是不能运行的。
public class LocalVariablesTableDemo2 {
public static void main(String[] args) {
int a;
System.out.println(a);//报错
}
}
public class LocalVariablesTableDemo2 {
static int a;//类变量
public static void main(String[] args) {
System.out.println(a);//输出0
}
}
还有一种成员变量,又叫做实例变量,它在实例化的时候被赋值。
4. Java虚拟机会使用局部变量表完成参数值到参数列表的传递过程,即实参到形参的传递。如果执行的是实例方法(没有被static)修饰,那么局部变量表的第0位索引的变量槽默认是用于传递方法所属对象的引用。可以通过this关键字来访问这个隐含的参数。
2.2 操作数栈
- 它是一个先入后出的栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。
- 当一个方法执行的时候,会有各种字节码指令往操作数栈中写入和提取内容。例如:在做算术运算的时候通过将运算设计的操作数压入栈顶后调用运算指令来执行。 或者调用方法时,通过操作数栈来进行方法参数的传递。
- 例如整数加法的iadd字节码指令,执行它时要求操作数栈最接近栈顶的两个元素已经存入两个int类型的数值,当执行这条指令时,会把这两个int值出栈并相加,然后将结果重新入栈。
- 操作数栈中的元素必须与字节码指令的序列严格匹配。iadd指令不能出现一个long和一个float使用iadd命令相加的情况。
- 两个栈帧可以有部分重叠。下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,进行方法调用的时候可以直接共用一部分数据,无须进行额外的参数复制传递了。
- Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",里面的栈就是操作数栈。
2.3 动态连接
- 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。
- Class文件的常量池中存在大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。
- 这些符号引用一部分会在类加载阶段或者是第一次使用的时候就被转化为直接引用。这种转化成为静态解析。另外一部分将在每一次运行期间转换为直接引用,这种转换就成为动态连接。
2.4 方法返回地址
- 当一个方法执行的时候,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候返回值可能会传递给上层的方法调用者。这种退出方式成为"正常调用完成"。
- 另外一种退出方式是在方法执行的过程中遇到了异常,并且这个异常没有在方法体内得到妥善处理。只要遇到了异常,如果在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。这种退出叫做"异常调用完成"
- 无论何种退出方式,都必须返回最初方法调用的位置,一般方法正常退出的时候,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。