虚拟机的结构
概述
Java虚拟机的基本功能要求就是能读取.class格式的文件,将执行里面定义的操作即可。其他的一些实现细节,如运行时数据区域的内存布局,使用的垃圾回收算法以及任何Java虚拟机指令的内部优化等,都与具体实现有关。
Java虚拟机也是操作两种类型的数据:原始数据类型和引用数据类型。所有的类型检测都在运行前由编译器完成,虚拟机不做数据类型检查。针对不同的数据类型,虚拟机都有对应的指令。
原始数据类型:
byte, short, int, long, char, float, double, boolean, returnAddress,其中returnAddress的值是指向Java虚拟机指令的操作码,与Java语言的类型无直接联系。
引用数据类型:
类,数组以及接口。
运行时的数据区域
1. pc寄存器:
每个虚拟机线程拥有自己的pc寄存器。任何时候,每个虚拟机线程都在执行本线程的方法,如果当前执行的方法不是本地方法,则pc寄存器包含虚拟机当前执行的地址,如果当前执行的方法是本地方法,则pc寄存器的值是未定义。pc寄存器足够宽,能容纳一个returnAddress或一个本地指针大小。
2. Java虚拟机栈
每个虚拟机线程有一个私有的栈,创建于虚拟机线程本身产生时。栈存储帧。栈与一般的编程语言(C语言)的栈作用类似:保存局部变量和中间结果,在方法调用和返回时扮演一定作用。一般不会对栈直接进行操作,除非执行push和pop帧操作,所以帧可以是基于堆分配的,栈所需的内存也不需要是连续的。
虚拟机栈的大小可以是固定的,也可以是动态调整的。对于栈大小是固定的情形,每个虚拟机栈的大小设置可以在栈创建的时候独立设置。而对于动态调整的情形,一般允许用户指定一个上限和一个下限。如果线程中的计算需要栈大小超过允许值,会抛出*Error异常。如果栈是动态可调整的,但是没有足够的内存,则会抛出OutOfMemoryError异常。
3. 堆
Java虚拟机有一个堆,它在所有虚拟机线程中是共享的。它是运行时的一个数据区域,提供所有类实例与已分配数组的内存。
堆在虚拟机启动的时候就创建了。对象所占用的堆存储空间被一个自动的存储管理系统回收(即垃圾回收系统)。对象从来不需要显示地析构。垃圾回收机制与具体的虚拟机实现强相关。
堆的大小可以是固定的,也可以是动态调整的。堆所占用的内存不需要是连续的。
4. 方法区域
Java虚拟机有一个方法区域,它是被所有虚拟机线程共享的。方法区域的作用类似于一般编程语言编译后的代码或一个UNIX进程的text段。它存储了每个类的结构如运行时常量池,域和方法数据以及方法和构造方法的代码,包含一个用于类,实例初始化和接口初始化的特殊方法。
5. 运行时常量池
一个运行时常量池是在.class文件中每个类或每个接口的运行时constant_pool表的表现形式。它包含几种类型的常量,如果编译阶段就已经知道的数字常量,以及运行时才能解析的域,方法。运行时常量池类似一般编程语言的符号表,不过它包含更大范围的数据。
每个运行时常量池是从Java虚拟机的方法区域分配的。一般是在类或接口创建的时候。
6. 本地方法栈
本地方法栈一般由Java虚拟机的指令解释器使用,通常本地栈在每个线程创建的时候分配。
7. 帧
帧用于存储数据和中间结果,同时也执行动态链接,返回方法调用结果以及分发异常。每当一个方法调用时,就会创建一个新的帧,当对应的方法调用结束,帧就会销毁。帧是在该帧的线程的虚拟机栈中分配的。每个帧都有自己的局部变量数组,自己的操作数栈,以及当前方法所在的类的运行时常量池的一个引用。
本地变量数组的大小以及操作栈在编译时就确定了。
任何时候,只有一个帧处于活跃状态,称之为当前帧,它的方法称为当前方法,方法所在的类称为当前类。当方法调用了另外一个方法或本身调用完成,则当前帧就不在是当前帧了。
帧可以扩展一些与具体实现相关的信息,如调试信息。
8. 局部变量
每个帧都包含称之为局部变量的数组。局部变量数组的长度在编译期间确定,并以一个类或接口以及与帧相关的方法的代码的二进制表示形式一起提供。
单个局部变量可以持有boolean, byte, char, short, int, float, reference或returnAddress类型。一对(两个局部变量的组合)局部变量可以持有long或double类型的值。
本地变量通过索引来寻址。第一个局部变量的索引值为零,通常为this。
9. 操作数栈
每个帧包含一个LIFO的栈,称为操作数栈。栈的最大深度在编译时已经确定。操作数栈在帧则创建时,是空的。Java虚拟机提供了指令从局部变量或域中加载常量或值到操作数栈中。虚拟机提供了指令从操作数栈中拿操作数,在他们上面执行操作,并将结果放进操作数栈。操作数栈也用于准备传递给方法的参数和接收方法的结果。
10. 动态链接
每个帧包含一个运行时常量池的引用。动态链接将代码中的符号方法引用转换成具体的方法引用,必要时加载类以解析当前仍末定义的符号,并将这些符号访问转换成合适的偏移值(与这些变量运行时位置相关的存储结构)。
Java虚拟机的内部示意图如下所示:
11. 特别命名的初始化方法
Java虚拟机中,每个类的构造方法都被视为一个名字为”< init >”的对象实例化方法。它只能通过Java虚拟机的特别指令:invokespecial调用。另外,类和接口的初始化方法也有一个特别的名字“< clinit >”,它只能由Java虚拟机隐式调用,不会被Java虚拟机指令直接调用。上述特别的命名是由编译器提供的。
12. 异常
Java虚拟机中的一个异常由Throwable或其子类的实例表示。异常可以是异常发生,也可以是同步发生。
13. 指令集
Java虚拟机指令由一般由一个字节长的操作码跟着零个或多个操作数组成。不考虑异常发生的话,总体执行逻辑如下:
do {
atomically calculate pc and fetch opcode at pc;
if (operands) fetch operands;
execute the action for the opcode;
} while (there is more to do);