1. 程式人生 > 其它 >第七篇 JVM之執行時資料區<3>: 區域性變量表

第七篇 JVM之執行時資料區<3>: 區域性變量表

區域性變量表是一組變數值的儲存空間,用於存放方法引數和方法內部定義的區域性變數

  虛擬機器棧由棧幀組成,棧幀由區域性變量表、運算元棧、動態連結和方法返回四部分組成,有的虛擬機器還有一些附加資訊。棧幀和Java方法對應,所以可以通過Java方法理解棧幀的各部分內容。

一、區域性變量表(Local Variables Table)

區域性變量表是一組變數值的儲存空間,用於存放方法引數和方法內部定義的區域性變數。區域性變量表的最大容量在編譯期間就已經確定,儲存在位元組碼檔案的Code屬性的locals裡面,並且在執行期間也不會改變。

public class LocalVariableTable {
    public int add(int x, int y) {
        
long i = 1L; { long j = 1; i = j; } int z = x + y; return z; } }
Code:
  stack=2, locals=7, args_size=3
     0: lconst_1
     1: lstore_3
     2: lconst_1
     3: lstore        5
     5: lload         5
     7: lstore_3
     8: iload_1
     
9: iload_2 10: iadd 11: istore 5 13: iload 5 15: ireturn LineNumberTable: line 5: 0 line 7: 2 line 8: 5 line 10: 8 line 11: 13 LocalVariableTable: Start Length Slot Name Signature 5 3 5 j J 0 16 0 this
Lvmstack/LocalVariableTable; 0 16 1 x I 0 16 2 y I 2 14 3 i J 13 3 5 z I

如上程式碼,上面是原始碼,下面為javap -v LocalVariableTable.class之後的位元組碼檔案的Code屬性部分。

  • stack=2是運算元棧的最大深度,locals=7是區域性變數的最大容量,args_size=3是方法引數的個數(因為是成員方法,所以除了x,y,還包含了this,靜態方法則沒有this),第一列數字的0-15則是方法執行時對應的JVM位元組碼指令地址,後面是位元組碼指令。
  • LineNumberTable則原Java方法和位元組碼指令的行號對應,這裡原Java方法第一行是5,最後一行是13,分別對應位元組碼的0-15行。
  • LocalVariableTable是區域性變量表,可以看到區域性變數在編譯期間就確定了變數內容,Start和Length是變數作用域的表示,Start是開始位置,0對應位元組碼指令的行號的0,Length是偏移量,Start=0和Length=16表示this這個變數在位元組碼指令0-15行這個作用域裡一直有效,Slot是變數槽的索引,意義後面補充,Name是變數名,Signature是變數型別,I表示為int型別,J是long型別。

變數槽(Slot)

 變數槽是區域性變量表的最小儲存單元,變數槽可以放置基本資料型別、引用資料型別、returnAddress資料型別。每一個變數槽都佔據的32bit的記憶體空間。

  • 基本資料型別:基本資料型別包括byte、boolean、short、int、float、long、double、char,變數槽只能儲存32bit,所以64bit的long、double用2個連續變數槽儲存,其他型別都用一個變數槽儲存。
  • 引用資料型別: 引用資料型別包括物件型別和陣列型別,變槽量儲存了引用型別的引用地址,佔用一個變數槽。
  • returnAddress資料型別:returnAddress指向了一條位元組碼指令的地址,也佔用一個變數槽,這種型別基本被虛擬機器淘汰。

區域性變量表是實際是一個數字陣列,每一個變數槽中都儲存了int型別的值,儲存內容都會轉為為int型別,boolean則用0和非0表示。每一個變數槽都有自己對應的索引,索引從0開始到最大長度處結束,如果是成員方法,this變數始終會佔據0索引對應的位置,對於long和double型別,則用兩個變數槽中的第一個索引對應,驗證這點可在上面的位元組碼指令程式碼看到i變數的Slot一列索引是3,之後是z變數的5並非4。

變數的作用域和Slot的複用:

變數本身是有作用域的,在上面Java方法程式碼中,將變數j放在程式碼塊中,j的作用域範圍就限制在程式碼塊內,變數槽本身是可以複用的,這樣可以提高儲存資源的利用,當程式計數器的值(當前要執行位元組碼指令的地址,位元組碼程式碼中Code屬性的第一列數字)超出了變數的作用域,變數槽就會交給其他變數複用,在位元組碼指令程式碼中Slot一列中可以看到j變數和z變數的索引都是5(j的變數作用域是5-8行,對應原Java方法的8-10行),說明z變數複用了j變數的變數槽。

變數槽的複用對GC的影響

JVM的垃圾回收會將沒有引用的物件回收,那超出變數作用域的物件會不會被回收?下面是一段摘抄自《深入理解Java虛擬機器》的程式碼和執行結果,程式碼內容為建立一個64M的位元組陣列物件,手動進行GC,執行這段程式碼時,可以設定JVM引數為-verbose:gc或者-XX:+PrintGC,就可以看到GC的簡單日誌。

   public static void main(String[] args) {
        {
            byte[] b = new byte[1024 * 1024 * 60];
        }
        System.gc();
    }
[GC (System.gc())  64000K->62208K(243712K), 0.0012488 secs]
[Full GC (System.gc())  62208K->62051K(243712K), 0.0041738 secs]

理論上GC時,已經超出了變數b的作用域範圍,其物件理應被GC掉,但實際並沒有,說明變數b和其物件的引用在超出變數範圍後依然存在,導致無法被GC。再來看下面程式碼和執行結果,在加入int a=0;之後,成功GC掉了陣列物件,說明b的變數槽被a複用,導致b與陣列物件引用被打斷,陣列物件成功被GC。所以超出作用域範圍的物件不一定會被回收,不過可以利用這點,當方法存在超出作用域大記憶體物件時,可以手動設定變數為null,從而讓物件可以被回收,但《深入理解Java虛擬機器》作者並不推薦,一是這種方式並不優雅,二是這種程式碼只存在試驗程式碼中,且賦null值操作在JVM編譯優化之後,看以看做無效操作

    public static void main(String[] args) {
        {
            byte[] b = new byte[1024 * 1024 * 60];
        }
        int a = 0;
        System.gc();
    }
[GC (System.gc())  64000K->62240K(243712K), 0.0010034 secs]
[Full GC (System.gc())  62240K->611K(243712K), 0.0041288 secs]    

執行緒安全問題:

棧幀不允許其他執行緒訪問,所以棧幀中的區域性變數也不存線上程安全問題。

區域性變數的值從哪裡來?
執行緒在執行Java方法時,首先會將方法引數放入到區域性變量表,對於方法內部的變數,基本型別的變數和值都會儲存在區域性變量表中,引用型別則先會在堆中建立物件,再將引用放入到區域性變數中,區域性變量表佔據棧幀的大部分記憶體,所以如果虛擬機器棧大小限定,區域性變數太多就可能會導致StackOverflowError異常。區域性變數在進入區域性變量表時並不會像靜態變數一樣有初始化的從操作,所以區域性變數並沒有初值,所以需手動設定初值,否則在呼叫改變數時編譯不通過。