字节码执行引擎
所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果
运行时栈帧结构
局部变量表
局部变量表(Local Variables Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,其容量最小单位是变量槽(Slot)。
一个Slot中可以存放:boolean,byte,char,short,int,float,reference,returnAddress (少见),虚拟机可通过局部变量表中的reference查找Java堆中的实例对象的起始地址/查找方法区中的Class对象。
若当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的变量槽就可以交给其他变量来重用
非static方法局部变量表的分配图解:
关于局部变量表中变量槽复用对垃圾收集的影响:
- 可以看到并无进行垃圾回收,因为System.gc执行时,bytes还处于作用域内
// VM参数:-verbose:gc
public static void main(String[] args) {
// 64M内存填充
byte[] bytes = new byte[64 * 1024 * 1024];
// 垃圾收集
System.gc();
}
输出结果:
[GC (System.gc()) 70739K->66368K(249344K), 0.0020881 secs]
[Full GC (System.gc()) 66368K->66168K(249344K), 0.0052478 secs]
- 给bytes加上括号{}后,作用域应该发生了变化,但可以看到垃圾依旧没有进行回。这是因为作用域虽然发生了变化,但局部变量表之后并没有再发生过任何的读写行为,那bytes原来占用着的变量槽也就还未被复用,GCRoots的部分局部变量表还存在关联,因此无法回收
// VM参数:-verbose:gc
public static void main(String[] args) {
{
byte[] bytes = new byte[64 * 1024 * 1024];
}
System.gc();
}
输出结果:
[GC (System.gc()) 70739K->66384K(249344K), 0.0009639 secs]
[Full GC (System.gc()) 66384K->66168K(249344K), 0.0059830 secs]
- 成功回收。因为变量a成功复用了bytes的变量槽
// VM参数:-verbose:gc
public static void main(String[] args) {
{
byte[] bytes = new byte[64 * 1024 * 1024];
}
int a = 0;
System.gc();
}
输出结果:
[GC (System.gc()) 70739K->66384K(249344K), 0.0011159 secs]
[Full GC (System.gc()) 66384K->632K(249344K), 0.0079671 secs]
值得注意的一个问题:局部变量不像类变量存在准备阶段(即有默认值),局部变量必须赋予初始化值才能使用
public static void main(String[] args) {
int a;
// 未赋值,编译报错
System.out.println(a);
}
操作数栈
操作数栈常叫操作栈,先入后出,32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2
在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作
动态连接
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为
静态解析
。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接
方法返回地址
方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。一般来说,方法正常退出时,主调方法的PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息