你未必出类拔萃,但一定与众不同
虚拟机字节码执行引擎
概述
执行引擎是Java虚拟机核心的组成部分之一,虚拟机是一个相对于物理机的概述,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器,缓存,指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自主实现的,因此可以不受物理条件制约地制定指令集宇执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈的栈元素,栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息,
每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机里从入栈到出栈的过程。
每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息,在编译Java程序源码的时候,栈帧中需要多大的局部变量表,需要多深的操作数栈就已经分析计算出来,并且写入到方法表的Code属性之中。
局部变量表
局部变量表示一组变量值的存储空间,用于存储方法参数和方法内部定义的局部变量。在Java程序被编译成Class文件的时候,就在方法 的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。
- 局部变量表的容量以变量槽为最小单位
- 每个变量槽 都应该能存放一个boolean,byte,char,short,int,float,reference或者returnAddress类型数据这八种数据类型
- 一个变量槽可以存放一个32位以内的数据类型。
- 对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间。
- Java虚拟机通过索引定位的方式使用局部变量表,索引值的范围是以0开始至局部变量表最大的变量槽数量。如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽,如果访问的是64位数据类型的变量,则说明会同时使用第N和N+1两个变量槽。对于相邻的共同存放一个64位数据的两个变量槽。
当一个方法被调用的时候,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递,如果执行的是实例方法,那么局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过this来访问到这个隐含的额参数,其余参数按照参数表顺序排列。
局部变量表表槽复用对垃圾收集的影响
例子1
public static void main(String[] args) {
byte[] bytes = new byte[64 * 1024 * 1024];
System.gc();
}
在idea中添加参数
然后运行得到结果
[GC (System.gc()) 70745K->66359K(249344K), 0.0030195 secs]
[Full GC (System.gc()) 66359K->66257K(249344K), 0.0063812 secs]
System.gc()运行以后并没有回收掉64MB的内存
例子2
例子1 没有回收掉bytes所占的内存是说得过去的,因为在执行的System.gc()时,变量bytes还处于作用域内,虚拟机自然不会回收掉64MB的内存,但是下面的例子还是没有回收掉
按道理bytes的作用域被限制花括号里面,运行System.gc()的时候应该是可以回收的,但是还是没有回收掉
public static void main(String[] args) {
{
byte[] bytes = new byte[64 * 1024 * 1024];
}
System.gc();
}
[GC (System.gc()) 70745K->66455K(249344K), 0.0013152 secs]
[Full GC (System.gc()) 66455K->66257K(249344K), 0.0061792 secs]
例子 3
此时在System.gc()前加一行 int a = 0;发现 内存被回收了
public static void main(String[] args) {
{
byte[] bytes = new byte[64 * 1024 * 1024];
}
int a = 0;
System.gc();
}
结果
[GC (System.gc()) 70745K->66423K(249344K), 0.0018609 secs]
[Full GC (System.gc()) 66423K->721K(249344K), 0.0048816 secs]
被回收的根本原因就是,局部变量表中的变量槽是否还存有关于bytes数组对象的引用,第一次修改中,代码虽然已经离开了bytes 的作用域,但在此之后并没有进行过任何对局部变量表的读写操作,bytes占用的局部变量表槽还没有被其他变量所复用,所以作为GCroots 的一部分的局部变量表依旧还有着对它的关联
因此我们可以通过手动先将其赋值为null。
操作数栈
操作数栈 后入先出
-
同局部变量表,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。
-
当一个方法开始执行的时候,这个方法的操作数栈是空的,在方法执行过程中则会有各种字节码指令往操作数栈写入和写出,也就是出栈和入栈操作
-
操作数栈中元素的的数据类型必须与字节码指令的序列严格匹配。编译代码时会检查一次,在类校验阶段的数据流分析时还会校验一次。
-
两个不同的栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的
但是在部分虚拟机中会做一些优化,使两个栈帧一部分重叠,不仅节约了空间,还可以共用一部分数据。
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
Class文件的常量池中存有大量的符号引用,字节码中的方法指令就以常量池里面指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另一部分会在每一次运行期间都转化为直接引用,这部分就是动态连接。
方法返回地址
当一个方法开始执行后,只有两种方式退出这个方法
- 遇到异常退出
- 正常调用完成
无论哪种方式退出,在方法退出后都必须返回到最初方法被调用时的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。
一般来说,方法正常退出,主调方法的PC计数器的值就可以作为返回地址,栈帧会保存这个计数器值,而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这个部分信息。