本文部分摘自《深入理解 Java 虚拟机》
执行引擎
执行引擎是 Java 虚拟机核心的组成部分之一,作用就是用来执行字节码。在 Java 虚拟机规范中执行引擎只是一个概念模型,不同的虚拟机可以有不同的实现,通常会有解释执行(通过编译器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,或者二者兼备。但无论是何种实现,从外观上看,所有 Java 虚拟机的执行引擎的输入、输出都是一致的:输入的是字节码的二进制流,处理过程是解析并执行字节码,输出是执行结果
运行时栈帧结构
Java 虚拟机以方法作为最基本的执行单元,栈帧则是用于支持虚拟机进行方法调用和方法执行背后的数据结构。一个方法从调用开始至执行结束的过程,都对应一个栈帧在虚拟机栈里面从入栈到出栈的过程。
每一个栈帧到包括了局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息,下面将逐一做详细介绍:
1. 局部变量表
局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在 Java 程序被编译为 Class 文件时,就在方法的 Code 属性的 max_locals 数据项中确定了该方法所需分配的局部变量表的最大容量。
局部变量表的容量以变量槽(Variable Slot)为最小单位,Java 虚拟机规范中并没有明确指出一个变量槽应占用的内存空间大小。对于 boolean、byte、char、short、int、float、reference 或 returnAddress 类型的数据,都可以使用 32 位或更小的物理内存来存储。而对于 64 位的数据类型,如 long 和 double,Java 虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。对于两个相邻的共同存放一个 64 位数据的变量槽,虚拟机不允许采用任何方式单独访问其中的某一个。
当一个方法被调用时,Java 虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递。如果执行的是实例方法,那局部变量表中第 0 位索引的变量槽默认是用于传递方法所属对象实例的引用,通过关键字 this 来访问这个隐含参数,其余参数则按照参数表顺序排列,占用从 1 开始的局部变量槽。
为了尽可能节省栈帧所耗用的内存空间,局部变量表中的变量槽是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码 PC 计数器的值已经超过了某个变量的作用域,那这个变量对应的变量槽就可以交给其他变量重用。不过这种设计会伴随有额外的副作用,例如在某些情况下变量槽的复用会直接影响到系统的垃圾收集行为
public static void main(String[] args) {
{
byte[] placeholder = new byte[64 * 1024 * 1024];
}
// int a = 0;
System.gc();
}
当执行到 System.gc() 时,虽然已经脱离 placeholder 的作用域,但由于变量槽还存在关于 placeholder 数组对象的引用,所以不会被回收。如果我们把 int a = 0
这段注释打开,那么原本 placeholder 对应的变量槽就会被其他变量复用,自然也就可以回收了。有时我们会看到手动将不再使用的变量置为 null 的操作,这并不见得是毫无意义的操作,可以将变量对应的局部变量槽清空。但在实际情况中,赋 null 值的操作在经过即时编译优化后几乎一定会被当成无效操作而被抹除,因此以恰当的变量作用域来控制变量的回收时间才是最优雅的解决手段。
2. 操作数栈
操作数栈(Operation Stack)是一个后进先出的栈结构,其最大深度也在编译时就已确定。当一个方法刚开始执行时,这个方法的操作数栈是空的。在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是入栈和出栈的操作。
3. 动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属的方法引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。我们知道,在 Class 文件的常量池中存在大量的符号引用,字节码中的方法调用指令就以常量池里指向的方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用时就被转化为直接引用,称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,称为动态连接。
4. 方法返回地址
当一个方法开始执行后,只有两种方式退出这个方法,要么正常结束,要么发生异常。无论采用何种退出方法的方式,都必须返回最初方法调用的位置,程序才能继续执行。一般来说,方法正常退出时,主调方法的 PC 计数器的值就可以作为返回地址,在栈帧中保存。而方法异常退出,返回地址是通过异常处理器表来确定的,栈帧一般不保存这个信息。
5. 附加信息
Java 虚拟机规范允许虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试、性能收集相关的信息,这部分信息完全取决于虚拟机的具体实现。