Java中方法的局部变量表(Local Variable Table)是一组变量的存储空间,用于存放方法的参数和方法内定义的局部变量,在Class编译的时候,Code属性中的 max_local值表示了该方法的最大局部变量表的长度。
1、局部变量的数量
1.1 实例方法的局部变量数量
为了方便演示,这里写了一个非常普通的Bean,代码如下
package com.zhoutao.bytefile;public class ExampleClass {private int a = 3;public int getA() {return a;}public void setA(int a) {this.a = a;}}
通过字节码工具 jclasslib-bytecode-viewer,可以非常清楚的看到总共有两个局部变量,this和a其中this的类型正是当前所在类的类型Lcom.zhoutao.bytefile.ExampleClass,a的类型为I,表示原生的int类型。
setter方法,其参数只有1个,为什么局部变量表的会有两个变量呢?
这是因为对于非静态方法而言,方法的第一个参数总是this,指向当前对象,这种将this作为参数传递给方法的方式,和Python的方法是第一个参数为self类似,只不过前者为隐式传递,后者为显示传递。
1.2 代码块中定义的局部变量
修改getA()方法的代码,调整为如下的,按照预想,应该有三个局部便令,this,s1以及s2。但正确的结果是有两个,this 和 s1, s2 并不是该方法的局部变量。
同样类似的还有在try{}语句块中定义的变量以及在finally定义的变量。
public int getA() {int s1 = 1;{int s2 = 1;}return a;}
1.3 异常捕获中的局部变量
继续修改getA() 方法的代码,添加一些异常处理的方式
public int getA() {try {} catch (RuntimeException e) {} catch (Exception e) {}return a;}
此时,getA() 方法的局部变量的数量为1个,而非3个,这是因为JVM会自动优化一些不必要的变量,所以e变量没有使用到,并不会作为局部变量出现在局部变量表中。
在catch的语句中添加对变量的使用,比如下面的操作
public int getA() {try {throw new ClassNotFoundException();} catch (RuntimeException e) {e.printStackTrace();} catch (ClassNotFoundException e1) {e1.printStackTrace();}return a;}
在异常处理添加了对异常的使用之后,JVM不在进行优化,所以也作为局部变量显示出来,所以这里可以观察到确实存在3个局部变量,分别是 this,e,e1
但是e和e1只能存在一个,当存在e的时候e1不可能存在,所以在Code属性中,最大的局部变量为2,而非3.
2、Slot 插槽
LocalVariableTable 以slot的最小内存单元,虚拟机并未指定slot占用多少个字节,仅描述,每个slot应该能存放一个 boolean,byte,char,short,int,float, reference,returnAddress 等类型的数据,这8种类型的数据都可以使用32位或者更小的内存空间来存放,所以一个64位的虚拟机也可以使用64位的slot位宽来实现Slot,但是要使用对齐和补白的手段来使之看上去和32位数据无异。
对于64位的数据类型,虚拟机会以高位对齐的手段来连续分配2个Slot的空间,来存放这些数据,64位的数据类型仅有float和long类型两种,这也与 'float & long类型的非原子协定'中一次把数据分割为2个32位的数据操作有些类似。
虚拟机通过索引的方式方位局部变量表,32位的系统中,索引为n就代表第n个slot,如果是64位系统,第则同时会使用n和n+1的slot两个Slot,不允许单独访问一个Slot,JVM规范中明确要求遇到这种字节码操作应该抛出异常。
同在字节码分析的章节异常,在LocalVariableTable的索引中,如果访问的是非实例方法(即类的方法)那么第0位表示的就是this 这与Python的面向对象编程非常相似。其余参数则按照顺序分配Slot,然后在根据方法表内部定义的顺序分配其余Slot,总结来说其分配顺序如下:this变量
方法中的其他形参
方法内部定义的局部变量
例如如下的代码的Slot分配如下
public class ExampleClass {public void exampleMethod(int a,int b,short c,char d){String a1= "";Object a2 = new Object();...}}
3、Slot 插槽复用机制以及影响
为了节省栈帧的使用空间,局部变量表中的Slot是可以重用的。有的局部变量的作用域并不会完全覆盖整个方法的作用域,如果当前PC计数器的值已经超出了某个变量的作用域,那么这个变量所对应的slot就会被重复使用 。这样设备的作用是为了节省栈帧内存空间,但在一些特殊情况下会影响系统的GC行为。
如下面的代码:
public static void main(String[] args) throws InterruptedException {{byte[] placeholder = new byte[64 * 1024 * 1024];}System.gc();// 为防止GC不及时,特延时1sTimeUnit.SECONDS.sleep(1);}
运行前在执行命令添加参数:-verbose:gc
[GC (System.gc()) 69468K->66104K(251392K), 0.0013540 secs][Full GC (System.gc()) 66104K->65938K(251392K), 0.0049892 secs]
按照道理程序在运行的时候,System.gc(); 发生的时候系统将会回收placeholder所占用的内存, 但是系统并没有正确的回收掉placeholder 所占用的内存。
修改程序,在System.gc(); 调用之前添加 int a = 0; 输出执行结果如下:
[GC (System.gc()) 69468K->66120K(251392K), 0.0019919 secs][Full GC (System.gc()) 66120K->402K(251392K), 0.0045493 secs]
如上面的显示可以看成,程序正常的回收内存 66120K->402K ,那么为什么 添加了 int a = 0; 这段无关紧要的代码就可以正常的GC 呢?
局部变量表所引用的变量占用的内存否可以引用,主要看局部变量所在的 Slot 是否仍在引用此 变量,在 System.gc(); 的时候,slot并未失去对 placeholder 的引用,所以无法正常的GC,在添加一个 int a = 0 之后,系统就可以正常回收这一段内存,这是因为在定义局部变量a之后,由于slot 复用他会将原placeholder所在的slot 复用,原内存地址不在被引用,所以可以正常的GC回收。