垃圾收集器与内存分配策略(25)


局部变量表
局部变量表的容量以变量槽( Slot)为最小单位,《Java虚拟机规范》中并没有明确指出一个变量槽应占用的内存空间大小,只是很有导向性地说到每个变量槽都应该能存放一个、byte、char、short、int、float、或类型的数据,这8种数据类型,都可以使用32位或更小的物理内存来存储,但这种描述与明确指出“每个变量槽应占用32位长度的内存空间”是有本质差别的,它允许变量槽的长度可以随着处理器、操作系统或虚拟机实现的不同而发生变化,保证了即使在64位虚拟机中使用了64位的物理内存空间去实现一个变量槽,虚拟机仍要使用对齐和补白的手段让变量槽在外观上看起来与32位虚拟机中的一致 。对于64位的数据类型,Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间 。Java语言中明确的64位的数据类型只有long和两种 。
Java虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的变量槽数量 。如果访问的是32位数据类型的变量,索引N就代表了使用第N个变量槽(第0个变量槽,放置的是this 。方法是静态的还是非静态的),如果访问的是64位数据类型的变量,则说明会同时使用第N和N+1两个变量槽 。对于两个相邻的共同存放一个64位数据的两个变量槽,虚拟机不允许采用任何方式单独访问其中的某一个 。
当一个方法被调用时,Java虚拟机会使用局部变量表来完成参数值到参数变量列表的传递过程,即实参到形参的传递 。如果执行的是实例方法(没有被修饰的方法),那局部变量表中第0位索引的变量槽默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数 。其余参数则按照参数表顺序排列,占用从1开始的局部变量槽,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的变量槽 。
为了尽可能节省栈帧耗用的内存空间,局部变量表中的变量槽是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的变量槽就可以交给其他变量来重用 。
操作数栈
操作数栈( Stack)也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO)栈 。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的数据项之中 。操作数栈的每一个元素都可以是包括long和在内的任意Java数据类型 。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2 。Javac编译器的数据流分析工作保证了在方法执行的任何时候,操作数栈的深度都不会超过在数据项中设定的最大值 。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作 。譬如在做算术运算的时候是通过将运算涉及的操作数栈压入栈顶后调用运算指令来进行的,又譬如在调用其他方法的时候是通过操作数栈来进行方法参数的传递 。举个例子,例如整数加法的字节码指令iadd,这条指令在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会把这两个int值出栈并相加,然后将相加的结果重新入栈 。
两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的 。但是在大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠 。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些空间,更重要的是在进行方法调用时就可以直接共用一部分数据,无须进行额外的参数复制传递了,重叠的过程如图