局部变量表Slot复用
为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。不过,这样的设计除了节省栈帧空间以外,还会伴随一些额外的副作用,例如,在某些情况下,Slot的复用会直接影响到JVM的垃圾收集行为,下为示例代码:
public static void main(String[] args) {
byte[] placeholder = new byte[1024 * 1024 * 64];
int i = 0;
System.gc();
}
[GC (System.gc()) 70779K->66264K(251392K), 0.0009475 secs]
[Full GC (System.gc()) 66264K->66059K(251392K), 0.0054671 secs]
上面代码很简单,即向内存填充了64MB的数据,然后通知虚拟机进行垃圾收集。在虚拟机运行参数中加上“-verbose:gc”,看看垃圾回收过程,发现在System.gc()运行后,并没有回收这64MB内存。
没有回收placeholder所占的内存能说得过去,因为在执行System.gc()时,变量placeholder还处在作用域之内,虚拟机自然不敢回收placeholder的内存。那我们把代码修改一下:
public static void main(String[] args) {
{
byte[] placeholder = new byte[1024 * 1024 * 64];
}
System.gc();
}
[GC (System.gc()) 70779K->66200K(251392K), 0.0008518 secs]
[Full GC (System.gc()) 66200K->66059K(251392K), 0.0055584 secs]
加入了花括号之后,placeholder的作用域被限制在花括号之内,从代码逻辑上讲,在执行System.gc()的时候,placeholder已经不可能再被访问了,但执行一下这段程序,会发现运行结果如上,这64MB的内存还是没有被回收。
在解析为什么之前,我们先对这段代码进行第二次修改,在调用System.gc()之前加入一行“int i = 0;”,如下:
public static void main(String[] args) {
{
byte[] placeholder = new byte[1024 * 1024 * 64];
}
int i = 0;
System.gc();
}
[GC (System.gc()) 70779K->66296K(251392K), 0.0009577 secs]
[Full GC (System.gc()) 66296K->523K(251392K), 0.0051850 secs]
这个修改看起来很莫名其妙,但运行一下程序,却发现这次内存真的被正确回收了。
在第一次修改后的代码中,placeholder能否被回收取决于:局部变量表中的Slot是否还存在关于placeholder数组对象的引用。第一次修改中,代码虽然已经离开了placeholder的作用域,但在此之后,没有任何对局部变量表的读写操作,placeholder原本所占用的Slot还没有被其他变量所复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联。