為了盡可能節省棧幀空間,局部變量表中的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一部分的局部變量表仍然保持著對它的關聯。