JVM学习之:虚拟机中的运行时栈帧小结(一)

JVM学习之:虚拟机中的运行时栈帧总结(一)

      每个人都知道,各种各样的动画视频,都是由一帧一帧图片连续切换结果的结果而产生的,其实虚拟机的运行和动画也类似,每个在虚拟机中运行的程序也是由许多的帧的切换产生的结果,只是这些帧里面存放的是方法的局部变量,操作数栈,动态链接,方法返回地址和一些额外的附加信息组成,在虚拟机中包含这些信息的帧称为“栈帧”,每个方法的执行,在虚拟机中都是对应的栈帧在虚拟机栈中的入栈到出栈的过程其中比较重要的一点时,如果虚拟机中同时有多个线程在执行,那么各个线程的栈帧都是相互独立,互不侵犯的,所以这也导致了,局部变量在多线程的环境下也是线程安全的

          一个方法的调用链可能会很长,于是当调用一个方法时,可能会有很多的方法都处于执行状态,但是对于执行引擎来讲,至于位于虚拟机栈顶的栈帧才是有效的,这个栈帧被称为当前栈这个栈帧所关联的方法称为当前方法,执行引擎的所有指令都是针对当前栈帧进行操作的。

       前面已经提到一个栈帧包括局部变量表,操作数栈,动态链接,方法返回地址和一些额外的附加信息组成,接下来对各个部分做一个简单的介绍。

(一)局部变量表

通过名字可以看出这个里面放的都是局部变量,例如方法参数,方法内部定义的局部变量。一般情况下,在java程序被编译为class文件的时候这个表的容量最大值就已经确定下来,是存在方法的Code属性的Max_locals数据项中

在局部变量表中Slot时最小的存储单位,虚拟机规范并没有明确指明一个Slot为多少位,Slot具体的大小也会随着操作系统和虚拟机的不同而不同,一般情况下可以当成时32位来看待,但是规定了一个Slot必须可以存放boolean,byte,char,int,float,reference(可能32位也可能时64位),returnAddress.而对于在虚拟机规范中被明确定义位64位的LongDouble而言,需要用两个连续的Slot来存放,由于时连个Slot来存储,所以在对LongDouble进行操作的时候就会存在原子性的问题,不过虚拟机会对它作出原子性保证(因为每个线程之间的栈帧是相互独立的,所以也不会由线程安全的问题)。

既然局部变量中存放了很多的局部变量,那么怎么来访问每个变量了?虚拟机规范中指出,虚拟机会利用索引编号的递增来对局部变量表中定义的变量进行依次访问(从0开始),而对于实例方法(非static方法),其局部变量表的第0个索引就是我们熟悉的this,这也是为什么在实例方法中我们可以使用this.name....的原因。

下面来谈谈Slot对虚拟机的垃圾回收的影响。由于在一个方法中,某个方法内的局部变量的作用范围也不一定可以覆盖整个方法,这就可能导致Slot资源的浪费,如果这个Slot对应的资源足够的大,那么Slot对资源的浪费也就可能会影响到整个虚拟机栈的使用,为了解决这个问题,虚拟机规范中规定了Slot的可重用性,即当一个方法中的某个局部变量超出了变量

 的有效范围时,那么那个变量的Slot可以被另外一个局部变量来使用。被重用的Slot便失去了和原来堆中实例的联系,这样堆中的实例便可以被垃圾回收器回收,当然一般情况下这些辅助的操作可能对系统性能的提升由很小的影响,但是,如果在那个局部变量“过期”之后还有很多的代码要执行,或者说后面由比较耗时的操作,而且在变量过期前,已经消耗了比较多的系统资源,那么这个辅助动作可能就非常有用了。

下面将通过三个例子来说明重用Slot对垃圾回收带来的好处

package com.eric.jvm.engineer;

public class SlotTest {
        /**
         * 主要验证重复利用Slot对于垃圾回收的帮助 
         ×1)运行参数:-verbose:gc -XX:+PrintGCDetails
         * 264M的对象大于了目前年轻代的空间,根据大对象直接进入老年代的原则,在观察结果的时候需要关注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);
        }