执行引擎是虚拟机最核心的组成部分之一。
运行时栈结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它属于虚拟机运行时数据区的虚拟机栈的栈元素,它存储了方法的局部变量表,操作数栈,动态链接和方法返回值等信息。每个方法的调用开始到执行结束,对应着栈帧的入栈和出栈。栈帧的大小是编译时确定的,并且写入到了方法表中的Code属性之中,一个栈帧需要的内存不会受到运行时变量数据的影响
局部变量表
它是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量,在Java代码编译成Class文件时,局部变量表的大小就在方法的Code属性的max_locals数据项确定了。它的最小容量单位是变量槽(Slot),虚拟机规范中没有明确指定它的大小,但是它应该能存放一个boolean,byte,char,short,int,float,reference或returnAddress类型的数据,slot一般会是32位。对于64位的数据long和double,虚拟机会以高位在前的方式分配连续的两个slot空间。
看到这里的时有个疑问,同样是64位,double却比long表示的数大。这是因为double牺牲了精度,扩大了范围,long可以表示从LONG.MIN_VALUE到LONG.MAX_VALUE任意一个整数,19位数;double由于有exp位,只能表示16位一下的精确整数,当数字大于16位时,精度有所损失。
由于局部变量表是线程私有的,因此分开读写两个slot虽然会破坏原子性,但是不会代码数据安全的问题。虚拟机使用索引定位的方式使用局部变量表索引值从0到最大的slot数。对于实例方法,第0号索引默认是对象实例的索引,可通过this指针来方法,之后按照顺序,先参数表,后是方法体内的局部变量。对于方法体的局部变量所占用的slot是可以重用的,当字节码PC计数器的值超出了某个变量的作用域,这个变量的slot就可以交给其他变量使用,这样可以节约栈空间,更重要的是它会影响垃圾回收,看下面的例子:
这样的话System.gc()
不会执行垃圾回收,这是因为p虽然不被使用了,但是没有其他的局部变量复用该slot,局部变量表中该slot保存的认识p指向64M的byte的引用。
操作数栈
它是一个后入先出的栈,最大深度在编译时确定,被写在Code属性中的max_stacks的数据项中,数据可以是任意的Java数据类型。会有各种字节指令想操作数栈中写入和提取内容,也就是入栈和出栈的操作。
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,可以支持方法调用过程中的动态连接。Class文件中常量池中存在很多符号引用,有些符号引用会在类加载阶段转化成直接引用,叫静态解析;另外一些会在运行期间转化成直接引用,称为动态连接