前言

最近在看《实战Java虚拟机》, 发现书上的一个关于局部变量表GC挺有意思,先上代码。

主角

没有GC

public class Main {
    public static void reversion(){
        {
            byte[] a = new byte[6*1024*1024];
        }
        System.gc();
    }

    public static void main(String[] args) {
        reversion();
    }
}

分配了一块6MB的堆空间,并使用局部变量引用这块空间, 然后显式进行一次Full GC。

image-20181029192234982

先配置一下JVM参数用于打印GC log

可以看到这块6MB的堆空间并没有被回收, 接下来加一行代码就能使得堆空间被回收。

可以GC

public class Main {
    public static void reversion(){
        {
            byte[] a = new byte[6*1024*1024];
        }
        int c = 0;
        System.gc();
    }

    public static void main(String[] args) {
        reversion();
    }
}

可以看到这6MB的空间已经被回收了,仅仅因为多了一句看似与a毫无关系的 int c = 0;

答案

借助jclasslib工具我们进一步查看函数的局部变量信息, 在此之前我们需要对代码做一点小改动再进行分析

public class Main {
    public static void reversion(){
        {
            byte[] a = new byte[6*1024*1024];
            System.out.println(a[0]);
        }
        int c = 0;
        System.gc();
    }

    public static void main(String[] args) {
        reversion();
    }
}

tips:JVM即时编译器拥有死代码消除的特性,a数组并没有被任何地方使用,即时编译器可以精简数据流,并且减少编译时间以及最终生成机器码的大小。简单的说如果a没有被使用的话会被编译成var0, 我们在本地变量表中看不到a了。

可以看到a和c在局部变量表中的索引值都是0,也就是说c重用了a在局部变量表中的槽位,从而使得a指向的堆空间能够被GC回收

栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能复用过期局部变量的槽位,从而达到节省资源的目的

总结

通过这个GC的小例子,切实感受到了JVM对于资源节省的严苛程度,对于作用域的细粒度把控之强大。给大家推荐我读的这本《实战Java虚拟机》,切实的一本好书,所谓好书无非两点:1.能读懂 2.有所获。同时也推荐极客时间的《深入拆解虚拟机》,多路学习,兼听则明。

最后打个小广告~


心源意码
75 声望13 粉丝