每个人都知道,各种各样的动画视频,都是由一帧一帧图片连续切换结果的结果而产生的,其实虚拟机的运行和动画也类似,每个在虚拟机中运行的程序也是由许多的帧的切换产生的结果,只是这些帧里面存放的是方法的局部变量,操作数栈,动态链接,方法返回地址和一些额外的附加信息组成,在虚拟机中包含这些信息的帧称为“栈帧”,每个方法的执行,在虚拟机中都是对应的栈帧在虚拟机栈中的入栈到出栈的过程。其中比较重要的一点时,如果虚拟机中同时有多个线程在执行,那么各个线程的栈帧都是相互独立,互不侵犯的,所以这也实现了局部变量在多线程的环境下也是线程安全的。
一个方法的调用链可能会很长,于是当调用一个方法时,可能会有很多的方法都处于执行状态,但是对于执行引擎来讲,至于位于虚拟机栈顶的栈帧才是有效的,这个栈帧被称为当前栈,这个栈帧所关联的方法称为当前方法,执行引擎的所有指令都是针对当前栈帧进行操作的。
前面已经提到一个栈帧包括局部变量表,操作数栈,动态链接,方法返回地址和一些额外的附加信息组成,接下来对各个部分做一个简单的介绍。
(一)局部变量表
通过名字可以看出这个里面放的都是局部变量,例如方法参数,方法内部定义的局部变量。一般情况下,在Java程序被编译为class文件的时候这个表的容量最大值就已经确定下来,是存在方法的Code属性的Max_locals数据项中
在局部变量表中Slot时最小的存储单位,虚拟机规范并没有明确指明一个Slot为多少位,Slot具体的大小也会随着操作系统和虚拟机的不同而不同,一般情况下可以当成时32位来看待,但是规定了一个Slot必须可以存放boolean,byte,char,int,float,reference(可能32位也可能时64位),returnAddress.而对于在虚拟机规范中被明确定义位64位的Long和Double而言,需要用两个连续的Slot来存放,由于时连个Slot来存储,所以在对Long和Double进行操作的时候就会存在原子性的问题,不过虚拟机会对它作出原子性保证(因为每个线程之间的栈帧是相互独立的,所以也不会由线程安全的问题)。
既然局部变量中存放了很多的局部变量,那么怎么来访问每个变量了?虚拟机规范中指出,虚拟机会利用索引编号的递增来对局部变量表中定义的变量进行依次访问(从0开始),而对于实例方法(非static方法),其局部变量表的第0个索引就是我们熟悉的this,这也是为什么在实例方法中我们可以使用this.name....的原因。
下面来谈谈Slot对虚拟机的垃圾回收的影响。由于在一个方法中,某个方法内的局部变量的作用范围也不一定可以覆盖整个方法,这就可能导致Slot资源的浪费,如果这个Slot对应的资源足够的大,那么Slot对资源的浪费也就可能会影响到整个虚拟机栈的使用,为了解决这个问题,虚拟机规范中规定了Slot的可重用性,即当一个方法中的某个局部变量超出了变量的有效范围时,那么那个变量的Slot可以被另外一个局部变量来使用。被重用的Slot便失去了和原来堆中实例的联系,这样堆中的实例便可以被垃圾回收器回收,当然一般情况下这些辅助的操作可能对系统性能的提升由很小的影响,但是,如果在那个局部变量“过期”之后还有很多的代码要执行,或者说后面由比较耗时的操作,而且在变量过期前,已经消耗了比较多的系统资源,那么这个辅助动作可能就非常有用了。
下面将通过三个例子来说明重用Slot对垃圾回收带来的好处:
示例代码:
public class SlotTest { /** * 主要验证重复利用Slot对于垃圾回收的帮助 ×(1)运行参数:-verbose:gc -XX:+PrintGCDetails * (2)64M的对象大于了目前年轻代的空间,根据大对象直接进入老年代的原则,在观察结果的时候需要关注ParOldGen * */ public static int M = 1024 << 10; public static void main(String[] args) { new SlotTest().test2(); } /* * replace 在执行gc操作的时候还没有超过它的作用域,也就是堆中还有实例和它直接关联所以不会被回收掉 * * [GC [PSYoungGen: 614K->352K(17856K)] 66150K->65888K(124224K), * 0.0024710 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC * (System) [PSYoungGen: 352K->0K(17856K)] [ParOldGen: * 65536K->65759K(106368K)] 65888K->65759K(124224K) [PSPermGen: * 2403K->2401K(21248K)], 0.0102720 secs] [Times: user=0.02 sys=0.00, * real=0.01 secs] */ public void test1() { // 64M byte[] replace = new byte[M << 6]; System.gc(); } /* * 在执行gc时,虽然replace已经过期,但是由于它的Slot中仍然存有相关的局部变量信息,所以gc 还是不可以 对64M的内存进行回收 * * [GC [PSYoungGen: 614K->288K(17856K)] 66150K->65824K(124224K), * 0.0019600 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [Full GC * (System) [PSYoungGen: 288K->0K(17856K)] [ParOldGen: * 65536K->65758K(106368K)] 65824K->65758K(124224K) [PSPermGen: * 2403K->2401K(21248K)], 0.0139210 secs] [Times: user=0.02 sys=0.00, * real=0.01 secs] */ public void test2() { { byte[] replace = new byte[M << 6]; } System.gc(); } /*在执行gc之前,由于a复用了replace 的Slot,所以此时可以认为replace在堆中的实例没有相关的引用,因此在gc的时候会将它回收 * [GC [PSYoungGen: 614K->368K(17856K)] 66150K->65904K(124224K), * 0.0019430 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC * (System) [PSYoungGen: 368K->0K(17856K)] [ParOldGen: * 65536K->223K(106368K)] 65904K->223K(124224K) [PSPermGen: * 2403K->2401K(21248K)], 0.0107030 secs] [Times: user=0.01 sys=0.01, * real=0.01 secs] */ public void test3() { { byte[] replace = new byte[M << 6]; } int a = 0; System.gc(); } }
对于上面代码中的test3(),也可以用replace=null来达到同样的效果。但是由于赋null值的操作在经过虚拟机JIT编译优化之后就会被消除掉,所以在这种情况下设置null值是没有意义的,其实就是test3()中的做法也是在特殊的情况下才会考虑的做法(后续的方法执行比较耗资源和时间,且前面的操作已经消耗了过多的资源),一般情况下只需要正确的保证每个局部变量有正确的变量作用域就可以了
最后要说明的是,由于局部变量不像实例变量或类变量那样会在准备阶段或者或者初始化阶段对其进行赋值,所以局部变量在没有赋值的情况下是不可以使用的,如果出现下面的情况,那么编译的时候就会提示“局部变量没有赋值.
public void test4(){ int a; System.out.println(a); }
(二)操作数栈:
首先根据名称可以看出操作数栈是一个基本的栈来实现数据结构,那么它自然也遵守栈的后入先出的原则.其次,它里面主要存放的是一些算数运算用到的参数也可能是中间结果,也可能是在调用其他方法时需要用到的参数,通过这点可以看出,方法刚刚开始执行的时候,这个里面是空的.最后 要说明的是操作数栈中可以存放任意的Java数据类型,包括long和double,且32位的数据类型占一个栈空间,64位的数据类型占2个栈空间.
(三)动态连接:
在说明什么是动态连接之前先看看方法的大概调用过程,首先在虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用,如果代表栈帧A的方法想调用代表栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法,这时候就有一点需要注意,如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,如果是在运行期间转换为直接引用,那么这种转换就成为动态连接。
(四)方法的返回地址
方法的返回分为两种情况,一种是正常退出,退出后会根据方法的定义来决定是否要传返回值给上层的调用者,一种是异常导致的方法结束,这种情况是不会传返回值给上层的调用方法.
不过无论是那种方式的方法结束,在退出当前方法时都会跳转到当前方法被调用的位置,如果方法是正常退出的,则调用者的PC计数器的值就可以作为返回地址,如果是因为异常退出的,则是需要通过异常处理表来确定.
在方法的的一次调用就对应着栈帧在虚拟机栈中的一次入栈出栈操作,因此方法退出时可能做的事情包括,恢复上层方法的局部变量表以及操作数栈,如果有返回值的话,就把返回值压入到调用者栈帧的操作数栈中,还会把PC计数器的值调整为方法调用入口的下一条指令。