JVM 之(15)局部變量表

        在《 JVM 之(1)運行時數據區》提到,虛擬機棧是 描述Java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。本篇主要分析局部變量表的原理結構。

        局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序被編譯爲Class文件時,就在方法的Code屬性的max_locals數據項中肯定了方法所須要分配的最大局部變量表的容量。

        局部變量表的容量以變量槽(Variable Slot)爲最小單位,虛擬機規範中並無明確指明一個Slot暫用的內存空間大小,只是頗有「導向性」地說明每一個Slot都應該能存放一個boolean,byte,char,short,int,float,refrence,returnAddress類型的數據,這種描述明確指出 「每一個Slot佔用32位長度的內存空間」 有一些差異,它容許Slot的長度隨着處理器,操做系統或虛擬機的不一樣而發生變化。不過不管如何,即便在64位虛擬機中使用64位長度的內存空間來實現Slot,虛擬機仍要使用對齊和補白的手段讓Slot在外觀上看起來和32位虛擬機中得一致。java

        既然前面提到了數據類型,在此順便說一下,一個Slot能夠存放一個32位之內的數據類型,Java中用32位之內的數據類型有:boolean,byte,char,short,int,float,reference,returnAddress八種類型。reference是對象的引用。虛擬機規範即沒有說明它的長度,也沒有明確指出這個引用應由怎樣的結構,通常來講,虛擬機實現至少都應當能今後引用中直接或間接的查找到對象在Java堆中得起始地址索引和方法區中得對象類型數據。而returnAddress是爲字節碼指令jsr,jsr_w 和 ret服務的。它指向了一條字節碼指令的地址。數組

        對於64位的數據類型,虛擬機會以高位在前的方式爲其分配兩個連續的Slot空間。Java語言中明確規定的64位的數據類型只有long和double數據類型分割存儲的作法與"long和double的非原子性協定" 中把一次long 和double 數據類型讀寫分割爲兩次32位讀寫的作法相似,在閱讀JAVA內存模型時對比下。不過,因爲局部變量表建在線程的堆棧上,是線程私有的數據,不管讀寫兩個連續的Slot是不是原子操做,都不會引發數據安全問題。安全

        虛擬機經過索引定位的方式使用局部變量表,索引值的範圍是從0開始到局部變量表最大的Slot數量。若是32位數據類型的變量,索引N就表明了使用第N個Slot,若是是64位數據類型的變量,則說明要使用第N個和N+1兩個Slot。ide

        在方法執行時,虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程。若是是實例方法(非static的方法),那麼局部變量表中第0位索引的Slot默認是用於傳遞方法所屬對象實例的引用,在方法中能夠經過關鍵字this來訪問這個隱含的參數,其他參數則按照參數表的順序來排列,暫用從1開始的局部變量Slot,參數表分配完畢後,在根據方法體內部定義的變量順序和做用域分配其他的Slot。優化

        局部變量表中得slot是可重用的,方法體定義的變量,其做用域並不必定會覆蓋整個方法體,若是當前字節碼PC計數器的值已經超過了某個變量的做用域,那麼這個變量對應的Slot就能夠交給其餘變量使用。這樣的設計不只僅是爲了節省棧空間,在某些狀況下Slot的複用會直接影響到系統的垃圾收集行爲。例如以下代碼:

this

[java]  view plain  copy
  1. /** 
  2.  * VM args: -verbose:gc 
  3.  *  
  4.  */  
  5. public class GCTest {  
  6.     public static void main(String[] args) {  
  7.         byte[] _64M = new byte[1024 * 1024 * 64];  
  8.         System.gc();  
  9.     }  
  10. }  
運行結果:

[GC 66558K->65952K(129024K), 0.0015650 secs]編碼

[Full GC 65952K->65853K(129024K), 0.0122710 secs]lua

從運行結果分析,發現System.gc()運行後並無回收掉這64M的內存。url

沒有回收掉"_64M"的內存能說的過去,由於在執行System.gc()時,變量_64M還處於做用域以內,虛擬機天然不敢回收掉該內存。咱們把代碼位以下:

[java]  view plain  copy
  1. /** 
  2.  * VM args: -verbose:gc 
  3.  *  
  4.  */  
  5. public class GCTest {  
  6.     public static void main(String[] args) {  
  7.         {  
  8.             byte[] _64M = new byte[1024 * 1024 * 64];  
  9.         }  
  10.         System.gc();  
  11.     }  
  12. }  
從代碼邏輯上將,在執行System.gc()的時候,變量「_64M」已經不可能在被訪問了,但執行如下這段程序,會發現運行結果以下:

[GC 66558K->65968K(129024K), 0.0014760 secs]spa

[Full GC 65968K->65853K(129024K), 0.0127180 secs]

這是爲何呢?

在解釋爲何以前,咱們先對代碼進行第二次修改。在調用 System.gc()以前加入代碼int x=0,  這個修改看起來莫名其妙,但運行如下程序,卻方法此次內存針對被正確回收了。

[java]  view plain  copy
  1. /** 
  2.  * VM args: -verbose:gc 
  3.  *  
  4.  */  
  5. public class GCTest {  
  6.     public static void main(String[] args) {  
  7.         {  
  8.             byte[] _64M = new byte[1024 * 1024 * 64];  
  9.         }  
  10.         int x=0;  
  11.         System.gc();  
  12.     }  
  13. }  

[GC 66558K->65936K(129024K), 0.0027120 secs]

[Full GC 65936K->317K(129024K), 0.0129600 secs]

局部變量"_64M"可否被回收的根本緣由就是:局部變量表中得Slot是否還存有關於_64M數組對象的引用。第一次修改,代碼雖然離開了_64的做用域,但在此以後,沒有任何對局部變量表的讀寫操做,_64M 本來所佔用的Slot尚未被其餘變量所複用,因此做爲GC Roots 一部分的局部變量表讓然保持對它的關聯。這種關聯沒有被及時打斷,在絕大部分狀況下都很輕微。但若是遇到一個方法,其後面的代碼有一些耗時很長的操做,而前面又佔用了大量的內存,實際上已經不會在被使用的變量,手工將其設置爲NULL值(用來代替int x=0)把變量對應的局部變量表Slot狀況,就不是一個毫無心義的操做,這種操做能夠做爲 一種在及特殊情形(對象暫用內存大,此方法的棧幀長時間不能被回收,方法調用次數達不到JIT編譯條件)下得「奇技」 來使用。但不該當對賦null值操做有過多的依賴,也沒有必要把它當作一個廣泛的編碼方法來推廣,以恰當的變量做用域來控制變量回收時間纔是最優雅的解決方法。

另外,賦null值的操做在通過虛擬機JIT編譯器優化以後會被消除掉,這時候將變量設置爲null其實是沒有意義的。字節碼被編譯爲bending代碼後,對GC Roots的枚舉也與解釋執行時期有所差異,在通過JIT編譯後,System.gc()執行時就能夠正確的回收掉內存。

打印GC詳細日誌還能夠加上參數:

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps



[java]  view plain  copy
  1. /** 
  2.  * VM args: -verbose:gc 
  3.  *  
  4.  */  
  5. public class GCTest {  
  6.     public static void main(String[] args) {  
  7.         byte[] _64M = new byte[1024 * 1024 * 64];  
  8.         System.gc();  
  9.     }  
  10. }  
運行結果:

[GC 66558K->65952K(129024K), 0.0015650 secs]

[Full GC 65952K->65853K(129024K), 0.0122710 secs]

從運行結果分析,發現System.gc()運行後並無回收掉這64M的內存。

沒有回收掉"_64M"的內存能說的過去,由於在執行System.gc()時,變量_64M還處於做用域以內,虛擬機天然不敢回收掉該內存。咱們把代碼位以下:

[java]  view plain  copy
  1. /** 
  2.  * VM args: -verbose:gc 
  3.  *  
  4.  */  
  5. public class GCTest {  
  6.     public static void main(String[] args) {  
  7.         {  
  8.             byte[] _64M = new byte[1024 * 1024 * 64];  
  9.         }  
  10.         System.gc();  
  11.     }  
  12. }  
從代碼邏輯上將,在執行System.gc()的時候,變量「_64M」已經不可能在被訪問了,但執行如下這段程序,會發現運行結果以下:

[GC 66558K->65968K(129024K), 0.0014760 secs]

[Full GC 65968K->65853K(129024K), 0.0127180 secs]

這是爲何呢?

在解釋爲何以前,咱們先對代碼進行第二次修改。在調用 System.gc()以前加入代碼int x=0,  這個修改看起來莫名其妙,但運行如下程序,卻方法此次內存針對被正確回收了。

[java]  view plain  copy
  1. /** 
  2.  * VM args: -verbose:gc 
  3.  *  
  4.  */  
  5. public class GCTest {  
  6.     public static void main(String[] args) {  
  7.         {  
  8.             byte[] _64M = new byte[1024 * 1024 * 64];  
  9.         }  
  10.         int x=0;  
  11.         System.gc();  
  12.     }  
  13. }  

[GC 66558K->65936K(129024K), 0.0027120 secs]

[Full GC 65936K->317K(129024K), 0.0129600 secs]

局部變量"_64M"可否被回收的根本緣由就是:局部變量表中得Slot是否還存有關於_64M數組對象的引用。第一次修改,代碼雖然離開了_64的做用域,但在此以後,沒有任何對局部變量表的讀寫操做,_64M 本來所佔用的Slot尚未被其餘變量所複用,因此做爲GC Roots 一部分的局部變量表讓然保持對它的關聯。這種關聯沒有被及時打斷,在絕大部分狀況下都很輕微。但若是遇到一個方法,其後面的代碼有一些耗時很長的操做,而前面又佔用了大量的內存,實際上已經不會在被使用的變量,手工將其設置爲NULL值(用來代替int x=0)把變量對應的局部變量表Slot狀況,就不是一個毫無心義的操做,這種操做能夠做爲 一種在及特殊情形(對象暫用內存大,此方法的棧幀長時間不能被回收,方法調用次數達不到JIT編譯條件)下得「奇技」 來使用。但不該當對賦null值操做有過多的依賴,也沒有必要把它當作一個廣泛的編碼方法來推廣,以恰當的變量做用域來控制變量回收時間纔是最優雅的解決方法。

另外,賦null值的操做在通過虛擬機JIT編譯器優化以後會被消除掉,這時候將變量設置爲null其實是沒有意義的。字節碼被編譯爲bending代碼後,對GC Roots的枚舉也與解釋執行時期有所差異,在通過JIT編譯後,System.gc()執行時就能夠正確的回收掉內存。

打印GC詳細日誌還能夠加上參數:

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps

相關文章
相關標籤/搜索