JVM 之 虚拟机栈 之 局部变量表(详细)

1. 基本概念

局部变量表:Local Variables,被称为局部变量数组或本地变量表 ,处于虚拟机栈中,如图所示:

JVM 之 虚拟机栈 之 局部变量表(详细)_第1张图片

 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型(byte、short、int、boolean、float、char、long、double)、对象引用(reference),以及 returnAddress 类型

 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题

局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的 Code 属性的 Maximum Local Variables 数据项中。在方法运行期间是不会改变局部变量表的大小的

方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。

局部变量表中的变量只在当前方法调用中有效在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁

2. 关于Slot的理解

JVM 之 虚拟机栈 之 局部变量表(详细)_第2张图片

  • 参数值的存放总是在局部变量数组的index 0开始,到数组长度-1的索引结束

  • 局部变量表最基本的存储单位是Slot(变量槽)

  • 局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。

  • 在局部变量表里,32位以内的类型只占用一个slot(包括引用类型,returnAddress类型),64位的类型(long和double)占用两个slot

    • byte、short、char在存储前被转换成int,boolean也被转换成int,0表示false,非0表示true。

    • long和double则占据两个slot

  • JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。

  • 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每个slot上。

  • 如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用其起始索引即可。(比如:访问long或者double类型变量),虚拟机不允许采用任何方式单独访问其中的某一个,如果遇到了这个问题,会在类加载的校验中抛出异常。

  • 如果当前帧是由构造方法或者实例方法(不是由static修饰的方法)创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。

3. 局部变量表Slot的复用对垃圾回收的影响

1. 首先,向内存中填充了64MB的数据,然后通知虚拟机进行垃圾收集。在IDEA中VM options 中加入“-verbose:gc” 参数,可以查看垃圾收集的过程,发现在System.gc()之后没有回收这个64MB的内存。

JVM 之 虚拟机栈 之 局部变量表(详细)_第3张图片

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

运行结果如下:

2. 我们可以分析到,因为在执行到System.gc()时,变量placeHolder仍然在作用域中,所以虚拟机不会回收。于是我们尝试修改代码如下:

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

结果如下: 

        我们发现placeHolder的作用域已经限制在花括号之内了,按照逻辑来讲,在执行System.gc()时,placeHolder已经不可能被访问到,为什么还不会回收这64MB的空间呢?

3. 我们做如下实验:

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

运行结果如下:

 我们可以发现,内存被回收了!这是为什么呢?

根本原因:局部变量表中的Slot是否还存在着有关于placeHolder数组对象的引用!

        第一次修改中,代码虽然已经离开了placcholder的作用城,但在此之后,再没有发生过任何对局部变量表的读写操作,placeholder原本所占用的变量槽还没有被其他变量所复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联,这就导致JVM在进行可达性分析时,认为不可回收。

        这种关联没有被及时打断,绝大部分情况下影响都很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量内存但实际上已经不会再使用的变量,这时候会产生影响。

        而此时我们可以手动将其设置为null值(用来代替那句int a=0,把变量对应的局部变量槽清空),这种操作可以作为一种在极特殊情形(对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到即时编译器的编译条件)下的“奇技”来使用。

补充:静态变量与局部变量的对比

变量的分类:

  • 数据类型分:基本数据类型、引用数据类型

  • 类中声明的位置分:成员变量(类变量,实例变量)、局部变量

  • 类变量:linking 的 prepare 阶段,给类变量默认赋值,init 阶段给类变量显示赋值即静态代码块

  • 实例变量:随着对象创建,会在堆空间中分配实例变量空间,并进行默认赋值

  • 局部变量:在使用前必须进行显式赋值,不然编译不通过。

我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。

和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。

JVM 之 虚拟机栈 之 局部变量表(详细)_第4张图片

局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

你可能感兴趣的:(JVM虚拟机栈,java,idea,intellij,idea)