浅谈虚拟机(2.1)凶器简介の局部变量表

局部变量表内存

  • 局部变量表是一组变量存储空间,用于存储方法参数方法内部定义的局部变量(local variable)。在java进程被编译为class文件的时候,就根据方法的code属性中max_locals数据项分配了局部变量表所需的最大空间。
  • 局部变量表以slot为最小单位,长度为32位,虚拟机规范中明确表明,每一个slot都应该能够存放一int,byte,char,short,boolean,float(注意这里六个基础类型中不包含long,double)。而64位的数据类型(long,double)则占用两个slot。
  • 在32位的JVM中,一个数据类型就占用一个slot;而在64位JVM中,则需要一个64位内存块模拟一个32位的slot。因此32位JVM比64位JVM更节省内存,但由于位数关系,64位JVM也是有极大价值的。
  • 局部变量表中存放的基础类型是值存储,如int i = 1,存放的是值;而引用类型则是存放着堆或者方法区中的数据首地址,如A a = new A(),存放的是new A()在堆中内存的首地址。
  • 未初始化的局部变量,即便声明了也不会存入局部变量表。该变量将从来没有进入过JVM,如int i;在内存中是没有区域的
  • 局部变量表是内存单元和值对应,内存单元有地址。因此变量名是不会存入局部变量表中的。
  • 在方法执行的时候,JVM是通过局部变量表完成变量值到变量列表的传递过程,从而JVM才能将变量进行映射找到。(试想一下,如果没有局部变量表,而是栈结构,或者是其他的如常量池方式来存放生命周期短、高频率使用的变量区域,其操作灵活性、性能会有什么区别?)

观察以下代码:

public class Test {  

    public static void main(String[] args){
        int param = 1;
        long l = 3;
        int i = 2;
        return;
    }

} 

这里写图片描述
从上图可以看到方法中code属性中有一张局部变量表,还有一个记录字节码偏移量到源代码之间距离的表。在右侧属性中,可以看到Maximum local variables:5,即局部变量表最大内存空间为5个slot。
这里写图片描述
从这里可以看到:

  • 虚拟机栈中存放了方法内存模型,该类中有两个方法。init和main,且init先于main方法。
  • main方法中有一张局部标量表,局部变量表中存放了四个变量:方法实参:args;局部变量:param、l、i。观察Index可以发现,args(String类型),param(int型)占用一个槽位即一个slot,而l(long型)占用了两个slot。
  • 在code属性中有描述该局部变量表最大内存空间为5个slot,args,param各占用1个slot,l占用2个slot,另外一个slot由i占用了。

局部变量在虚拟机中的生命周期

局部变量表存放于虚拟机栈中,其内存空间的回收是怎样进行的?我们可以通过以下代码来学习:

public class Test {  
    public static void main(String[] args){
        {
            byte[] _64M = new byte[1024*1024*64];
        }
        System.gc();
        return;
    }
} 

gc.log(垃圾回收日志)结果为:

0.156: [GC (System.gc())  67532K->66176K(125952K), 0.0007981 secs]
0.157: [Full GC (System.gc())  66176K->66060K(125952K), 0.0043201 secs]

从上面可以看到,无论是小GC还是Full GC,虚拟机中始终有65MB上下的大小。我们定义的变量_64M(大小为65MB,因为是一个64 M个元素的byte数组)并没有被回收。当垃圾回收器工作的时候,_64M已经不在方法块中,它的生命周期在”}”就该结束,垃圾回收器将会将它回收处理才对,但是此处却并没有被回收。
再做一下个对比:

public class Test {  

    public static void main(String[] args){
        {
            byte[] _64M = new byte[1024*1024*64];
        }
        int a = 1;
        System.gc();

        return;
    }

} 

在调用System.gc()之前加上一行代码int a = 1;
此时gc.log的结果为:

0.123: [GC (System.gc())  67532K->66144K(125952K), 0.0014830 secs]
0.125: [Full GC (System.gc())  66144K->524K(125952K), 0.0051350 secs]

可以看到,小GC同样没有将_64M回收,但Full执行完毕以后,虚拟机内存减少了64.082MB,_64M被垃圾收集器回收了。为什么会这样?
在第一段代码中,变量_64M虽然不在生命周期范围内,无法被访问,但是该变量所占用的slot并未被其他变量复用,因此GC Roots仍然认为保持着对这一大块内存的可达性联系,不进行垃圾回收。(本机虚拟机为HotSpot,垃圾回收算法为可达性算法,详解此处不多讲了)
但是在第二段代码中,在GC之前添加了int a = 1;的一行代码,此时变量a将复用_64M的内存,GC会首先清楚_64M的内存空间,然后再在_64M的slot中分配一个最合适位置的slot给a(int变量4个字节,32位正好一个slot可以存储。最合适位置意指在逻辑上与内存变量表连续的下一个slot,而这个slot在物理上可能是不连续的。)

想一想
在这个地方,博主做了另一个测试,当用int a;来释放_64M的时候,是失败的。这是为什么呢?
局部变量表
从这里可以看到,局部变量表中没有a这个变量。其实,局部变量如果没有进行初始化,那么将不会为其分配内存空间。

心得:用这种方式来释放局部变量,通常是不推荐的,大量的使用会使代码结构和可读性变得非常差。最好还是利用合适的作用域来控制变量的分配和回收。
但是,当有些时候方法中的后续代码工作量繁琐庞大的时候,如方法中又多层次地调用了其他方法,这种由于未被复用的占内存庞大的槽位(示例代码中占用了整整64MB)可能会严重影响后续代码的装入和运行,甚至导致内存溢出程序崩溃。此时使用这种方式来释放内存,将会起到神奇的作用。

注意:

  • 局部变量表是根据索引随机访问的数据结构,索引从0号位置开始
  • 非静态方法的局部变量表第一个内存单元存储的是this指针
  • 方法入参优先于方法内部本地变量存入局部变量表

总结

JVM->虚拟机栈->栈帧->局部变量表
从上面的结构可以看到,java虚拟机中有虚拟机栈,虚拟机栈是用来存放方法内存模型的内存区域,而每个方法都会有一个栈帧,每个栈帧中又会有一张局部变量表。所以,局部变量表中存放的变量,是其对应方法的变量,来源有两个:方法实参和方法的局部变量。
有些时候出现内存溢出问题,不妨留意一下方法中占用较大内存的变量是否出现超出生命周期而未被释放的情况(尽管很少会有)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值