JVM入门--09 局部变量表

局部变量表是栈帧的重要组成部分之一。它用于保存函数的参数以及局部变量。局部变量表中的变量只在当前函数调用中有效,当函数调用结束后,随着函数栈帧的销毁,局部变量表也会随之销毁。

由于局部变量表在栈帧之中,因此,如果函数的参数和局部变量较多,会使得局部变量表膨胀,从而每一次函数调用就会占用更多的栈空间,最终导致函数的嵌套调用次数减少。

下面的代码演示了这种情况,第 1 个 recursion()函数含有 3 个参数和 10 个局部变量,因此,其局部变量表含有 13 个变量。而第 2 个 recursion()函数不含有任何参数和局部变量。当这两个函数被嵌套调用时,第 2 个 recursion()函数可以拥有更深的调用层次。

package com.javaclimb.test;

public class TestStackDeep {
    private static int count = 0;

    public static void recursion(long p1, long p2, long p3) {
        long i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5, i6 = 6, i7 = 7, i8 = 8, i9 = 9, i10 = 10;
        count++;
        recursion(p1,p2,p3);
    }

    public static void recursion(){
        count++;
        recursion();
    }

    public static void main(String[] args) {
        try{
            recursion(0L,0L,0L);
//            recursion();
        }catch (Throwable e){
            System.out.println("调用深度:"+count);
            e.printStackTrace();
        }
    }

}

使用参数-Xss128K 执行上述代码中的第 1 个 recursion()函数,输出结果如下:

调用深度:302
java.lang.StackOverflowError
	at com.ruoyi.web.controller.system.TestStackDeep.recursion(TestStackDeep.java:7)

使用参数-Xss128K 执行上述代码中的第 2个 recursion()函数,输出结果如下:

调用深度:1096
java.lang.StackOverflowError
	at com.ruoyi.web.controller.system.TestStackDeep.recursion(TestStackDeep.java:13)

可以看到,在相同的栈容量下,局部变量少的函数可以支持更深的函数调用。

使用 jclasslib 工具可以更进一步查看函数的局部变量信息。下图显示了第一个 recursion()函数的最大局部变量表的大小为 26 个字。因为该函数包含总共 13 个参数和局部变量,且都为long 型,long 和 double 在局部变量表中需要占用 2 个字,其他如 int、short、byte、对象引用等占用 1 个字。

说明: 字 (Word ) 指的是计算机内存中占据一个单独的内存单元编号的一组二进制串。8位是1字节, 32 位计算机上一个字为 4个字节长度,64 位计算机上一个字为8个字节长度。

下图显示了在 Class 文件中的局部变量表的内容 (这里说的局部变量表和上述的局部变量表不同,这里指 Class 文件的一个属性,而上述的局部变量表指 Java 栈空间的一部分) 。

可以看到,在 Class 文件的局部变量表中,显示了每个局部变量的作用域范围、所在槽位的序号 、名字和数据类型 (J表示 long 型) 。

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

下面的代码显示了局部变量表槽位的复用。在 localvar1()函数中,局部变量 a 和 b 都作用到了方法的末尾,故 b 无法复用 a 所在的位置。而在 localvar2()函数中,局部变量 a 在第 14 行时不再有效,故局部变量b 可以复用 a 的槽位(1个字)。

package com.javaclimb.test;

public class TestReuse {
    public void localvar1() {
        int a = 0;
        System.out.println(a);
        int b = 0;
    }

    public void localvar2() {
        {
            int a = 0;
            System.out.println(a);
        }
        int b = 0;
    }
}

下图显示了localvar1()的局部变量信息,该函数最大局部变量大小为 3 字,第 0 个槽位为方法的 this 引用(实例方法的第一个局部变量都是 this 引用),第 1 个槽位为变量 a,第 2 个槽位为变量 b,每个变量占 1 字,合计 3 字。

下图显示了localvar2(0)的局部变量信息, 该函数的最大局部变量表为2字, 虽然和localvar1()一样,拥有 this、a、b 等 3 个局部变量,但b 复用了 a 的槽位(从它们都占用了第 1 个槽位可以知道这点),因此在整个函数执行中,同时存在的最大局部变量为2字。

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都是不会被回收的。因此,理解局部变量表对理解垃圾回收也有一定帮助。

下面通过一个简单的示例,展示局部变量对垃圾回收的影响。
 

package com.javaclimb.test;

public class LocalVarGC {

    public static void main(String[] args) {
        LocalVarGC ins = new LocalVarGC();
        ins.localvarGc1();
        ins.localvarGc2();
        ins.localvarGc3();
        ins.localvarGc4();
        ins.localvarGc5();
    }

    public void localvarGc1() {
        System.out.println(1);
        byte[] a = new byte[6 * 1024 * 1024];
        System.gc();
    }

    public void localvarGc2() {
        System.out.println(2);
        byte[] a = new byte[6 * 1024 * 1024];
        a = null;
        System.gc();
    }

    public void localvarGc3() {
        System.out.println(3);
        {
            byte[] a = new byte[6 * 1024 * 1024];
        }
        System.gc();
    }

    public void localvarGc4() {
        System.out.println(4);
        {
            byte[] a = new byte[6 * 1024 * 1024];
        }
        int c = 10;
        System.gc();
    }

    public void localvarGc5() {
        System.out.println(5);
        localvarGc1();
        System.gc();
    }
}

上述代码中, 每一个 localvarGcN()函数都分配了一块 6MB 的堆空间, 并使用局部变量引用这块空间。

在 localvarGc1()中,在申请空间后,立即进行垃圾回收,很明显,由于 byte 数组被变量 a 引用,因此无法回收这块空间。

在 localvarGc2()中,在垃圾回收前,先将变量 a 置为 null,使 byte 数组失去强引用,故垃圾回收可以顺利回收 byte 数组。

对于 localvarGc3(),在进行垃圾回收前,先使局部变量 a 失效,虽然变量 a 已经离开了作用域,但是变量 a 依然存在于局部变量表中,并且也指向这块 byte 数组,故 byte 数组依然无法被回收。

对于 localvarGc4(),在垃圾回收之前,不仅使变量 a 失效,更是声明了变量 c,使变量 c 复用了变量 a 的字,由于变量 a 此时被销毁,故垃圾回收器可以顺利回收 byte 数组。

对于 localvarGc5(),它首先调用了 localvarGc1(),很明显,在 localvarGc1()中并没有释放byte 数组,但在 localvarGc1()返回后,它的栈帧被销毁,自然也包含了栈帧中的所有局部变量,故byte 数组失去引用,在 localvarGc5()的垃圾回收中被回收。

我们可以使用参数-XX:+PrintGC 执行上述几个函数,在输出的目志中,可以看到垃圾回收前后堆的大小,进而推断 byte 数组是否被回收。下面是运行结果:

1
[GC (System.gc())  24085K->8378K(251392K), 0.0067928 secs]
[Full GC (System.gc())  8378K->8146K(251392K), 0.0098561 secs]
2
[GC (System.gc())  15601K->8146K(251392K), 0.0009249 secs]
[Full GC (System.gc())  8146K->1820K(251392K), 0.0076060 secs]
3
[GC (System.gc())  11897K->7996K(251392K), 0.0024737 secs]
[Full GC (System.gc())  7996K->7758K(251392K), 0.0137248 secs]
4
[GC (System.gc())  15213K->7758K(251392K), 0.0006400 secs]
[Full GC (System.gc())  7758K->1512K(251392K), 0.0127536 secs]
5
1
[GC (System.gc())  8967K->7656K(251392K), 0.0026574 secs]
[Full GC (System.gc())  7656K->7656K(251392K), 0.0072722 secs]
[GC (System.gc())  7656K->7656K(251392K), 0.0003367 secs]
[Full GC (System.gc())  7656K->1512K(251392K), 0.0066111 secs]


 

  • 9
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值