(六)對象內存佈局

Java 中創建對象的方式有多種:new 語句、反射機制、Object.clone 方法、反序列化以及 Unsafe.allocateInstance 方法來新建對象。其中,Object.clone 方法和反序列化通過直接複製已有的數據,來初始化新建對象的實例字段。Unsafe.allocateInstance 方法則沒有初始化實例字段,而 new 語句則是通過調用構造器來初始化實例字段:

  // Foo foo=new Foo();
  0: new           #2       // class Foo
  3: dup
  4: invokespecial #3       // Method "<init>":()V
  7: astore_1
  8: return

構造器語法規則:

  1. 當一個類中無構造器,編譯器自動添加一個無參的構造器。
  2. 子類的構造器需要調用父類的構造器。如果父類存在無參數構造器的話,該調用可以是隱式的,也就是說 Java 編譯器會自動添加對父類構造器的調用。但是,如果父類沒有無參數構造器,那麼子類的構造器則需要顯式地調用父類帶參數的構造器。
public Foo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1   // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0

總而言之,當我們調用一個構造器時,它將優先調用父類的構造器,直至 Object 類。這些構造器的調用者皆爲同一對象,也就是通過 new 指令新建而來的對象,它的內存其實涵蓋了所有父類中的實例字段。也就是說,雖然子類無法訪問父類的私有實例字段,或者子類的實例字段隱藏了父類的同名實例字段,但是子類的實例還是會爲這些父類實例字段分配內存的。

 

類型指針

在 Java 虛擬機中,每個 Java 對象都有一個對象頭(object header),其由標記字段類型指針組成。其中,標記字段存儲 Java 虛擬機中有關該對象的運行時數據,比如哈希碼、GC 信息和鎖信息;類型指針則指向該對象的類。

在 64 位虛擬機中,標記字段和類型指針分別佔用 64 位,也就是說,每一個 Java 對象的額外內存開銷是 16 字節。

爲了儘量較少對象的內存使用量,64 位 Java 虛擬機引入了壓縮指針的概念(對應虛擬機選項 -XX:+UseCompressedOops,默認開啓),將堆中原本 64 位的 Java 對象指針壓縮成 32 位的。這樣一來,對象頭內存大小由原來的 16 字節減少到 12 字節。

 

壓縮指針的原理

我們做個類比,把 Java 對象比作車,內存比作停車場,內存地址就是停車位,一輛車佔兩個停車位。現在,我們按照順序給它們編號。也就是說,停在 0 號和 1 號停車位上的叫 0 號車,停在 2 號和 3 號停車位上的叫 1 號車,依次類推。

原本地址尋址方式是按照車位號。4,5號車位上存放的是 4/2=2 號車。現在我們規定指針裏存的值是車號,2號車存放在2x2=4,5號車位上這樣一來,32 位壓縮指針最多可以標記 2 的 32 次方輛車,對應着 2 的 33 次方個車位。當然,房車也有大小之分。大房車佔據的車位可能是三個甚至是更多。不過這並不會影響我們的尋址算法:我們只需跳過部分車號,便可以保持原本車號 *2 的尋址系統,如上圖。

上述模型有一個前提,你應該已經想到了,就是每輛車都從偶數號車位停起。這個概念我們稱之爲內存對齊(對應虛擬機選項 -XX:ObjectAlignmentInBytes,默認值爲 8)。

默認情況下,Java 虛擬機堆中對象的起始地址需要對齊至 8 的倍數。如果一個對象用不到 8N 個字節,那麼空白的那部分空間就浪費掉了。這些浪費掉的空間我們稱之爲對象間的填充(padding)。

在默認情況下,Java 虛擬機中的 32 位壓縮指針可以尋址到 2 的 35 次方個字節(2 的 32 次方乘以 8),也就是 32GB 的地址空間(超過 32GB 則會關閉壓縮指針)。

在對壓縮指針解引用時,我們需要將其左移 3 位,再加上一個固定偏移量,便可以得到能夠尋址 32GB 地址空間的僞 64 位指針了。

此外,我們可以通過配置剛剛提到的內存對齊選項(-XX:ObjectAlignmentInBytes)來進一步提升尋址範圍。但是,這同時也可能增加對象間填充,導致壓縮指針沒有達到原本節省空間的效果。

當然,就算是關閉了壓縮指針,Java 虛擬機還是會進行內存對齊。此外,內存對齊不僅存在於對象與對象之間,也存在於對象中的字段之間。比如說,Java 虛擬機要求 long 字段、double 字段,以及非壓縮指針狀態下的引用字段地址爲 8 的倍數。

字段內存對齊的其中一個原因,是讓字段只出現在同一 CPU 的緩存行中。如果字段不是對齊的,那麼就有可能出現跨緩存行的字段。也就是說,該字段的讀取可能需要替換兩個緩存行,而該字段的存儲也會同時污染兩個緩存行。這兩種情況對程序的執行效率而言都是不利的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章