天天看點

JOL:Java 對象記憶體布局

作者:JU幫
JOL:Java 對象記憶體布局

如果想要深入的學習 synchronized 關鍵字,必須提前掌握的一部分知識就是 Java 對象記憶體布局。通過這篇文章一起探索 Java 對象在虛拟機中是如何儲存的。

在正式學習後續内容之前,先約定如下:

  • Java 對象:本篇中所說的 Java 對象是指普通 Java 對象,不包括數值對象、Class 對象
  • 虛拟機:除了特别說明以外,虛拟機均指 HotSpot 虛拟機

Java 對象記憶體布局

在 HotSpot 虛拟機裡,對象在堆記憶體中的存儲布局可以劃分為三個部分:對象頭(Header)、執行個體資料(Instance Data)和對齊填充(Padding)。

JOL:Java 對象記憶體布局

Header

Java 對象頭包括兩個部分:Mark Word 和 Class Pointer,對于數組對象對象頭還包括數組長度(Length),下面具體看一下每個部分:

1 Mark Word

用于存儲對象自身的運作時資料,如哈希碼(HashCode)、GC 分代年齡、鎖狀态标志、線程持有的鎖、偏向線程 ID、偏向時間戳等,這部分資料的長度在 32 位和 64 位的虛拟機(未開啟壓縮指針)中分别為 32 個比特和 64 個比特。

對象需要存儲的運作時資料很多,其實已經超出了 32、64 位 Bitmap 結構所能記錄的最大限度,但對象頭裡的資訊是與對象自身定義的資料無關的額外存儲成本,考慮到虛拟機的空間效率,Mark Word 被設計成一個有着動态定義的資料結構,以便在極小的空間記憶體儲盡量多的資料,根據對象的狀态複用自己的存儲空間。

Mark Word 在不同狀态時存儲的資料如下所示(32 和 64 位虛拟機):

JOL:Java 對象記憶體布局
JOL:Java 對象記憶體布局
在某一時刻 Mark Word 隻會處于上圖中某一個鎖狀态,根據目前對象 synchronized 鎖更新的不同鎖狀态,Mark Word 儲存的資料會不同,這就為什麼說 Mark Word 是動态定義的資料結構。具體 synchronized 章節講解。

2 Class Pointer

這部分是一個類型指針,即對象指向它的類型中繼資料的指針,Java 虛拟機通過這個指針來确定該對象是哪個類的執行個體。

并不是所有的虛拟機實作對象頭都具有類型指針,這和對象的通路定位方式有關,主流的通路方式主要有使用句柄和直接指針兩種:

    • 使用句柄的方式:Java 堆中将可能會劃分出一塊記憶體來作為句柄池,reference 中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自具體的位址資訊
    • 直接指針的方式:Java 堆中對象的記憶體布局就必須考慮如何放置通路類型資料的相關資訊,reference 中存儲的直接就是對象位址,如果隻是通路對象本身的話,就不需要多一次間接通路 的開銷(HotSpot 虛拟機采用該方式,是以對象頭中有類型指針用于存放對象結構的引用)
JOL:Java 對象記憶體布局

通過句柄通路對象

JOL:Java 對象記憶體布局

通過直接指針通路對象

3 Length

如果對象是一個 Java 數組,那在對象頭中還必須有一塊用于記錄數組長度的資料,因為虛拟機可以通過普通 Java 對象的中繼資料資訊确定 Java 對象的大小,但是如果數組的長度是不确定的,将無法通過中繼資料中的資訊推斷出數組的大小。

這就解釋了為什麼 Java 數組一旦初始化了數組長度,就不能修改

Instance Data

執行個體資料部分是對象真正存儲的有效資訊,即我們在程式代碼裡面所定義的各種類型的字段内容,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄起來。這部分的存儲順序會受到虛拟機配置設定政策參數(-XX:FieldsAllocationStyle參數)和字段在 Java 源碼中定義順序的影響。 HotSpot 虛拟機預設的配置設定順序為 longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),從以上預設的配置設定政策中可以看到,相同寬度的字段總是被配置設定到一起存放,在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。如果 HotSpot 虛拟機的 +XX:CompactFields 參數值為 true(預設就為 true),那子類之中較窄的變量也允許插入父類變量的空隙之中,以節省出一點點空間。

Padding

對象的第三部分是對齊填充,這并不是必然存在的,也沒有特别的含義,它僅僅起着占位符的作用。由于 HotSpot 虛拟機的自動記憶體管理系統要求對象起始位址必須是 8 位元組的整數倍,換句話說就是任何對象的大小都必須是 8 位元組的整數倍。對象頭部分已經被精心設計成正好是 8 位元組的倍數(1 倍或者 2 倍),是以,如果對象執行個體資料部分沒有對齊的話,就需要通過對齊填充來補全。

JOL 工具

JOL 的全稱是 Java Object Layout。是一個用來分析 JVM 中對象記憶體布局的小工具。包括對象在記憶體中的占用情況,執行個體對象的引用情況等等。

使用 JOL 需要在 Maven 項目中引入依賴:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
</dependency>           
JOL 高版本依賴和低版本輸出對象記憶體布局時略有不同,但是高版本對對象布局描述更加直覺。

測試沒有任何字段的類的代碼如下:

public class JolTest {
    public static void main(String[] args) {
        Console.log("===================== VM DESC =====================");
        Console.log(VM.current().details());

        Console.log("===================== Java Object Layout =====================");
        Console.log(ClassLayout.parseInstance(new Object()).toPrintable());
    }
}           

輸出結果:

JOL:Java 對象記憶體布局

通過上面的輸出結果可以擷取如下資訊:

1、VM DESC

  • Running 64-bit HotSpot VM:運作的虛拟機是 64 位 HotSpot 虛拟機
  • Objects are 8 bytes aligned:對象是基于 8 byte 對齊
  • Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] 和 Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]:描述了對象字段和數組元素各種資料類型占用的記憶體大小,依次是:4 - 引用資料類型占用 4 byte(因預設開啟壓縮指針);1 - boolean 占用 1 byte;1 - byte 占用 1 byte;2 - short 占用 2 byte;2 - char 占用 2 byte;4 - int 占用 4 byte;4 - float 占用 4 byte;8 - long 占用 8 byte;8 - double 占用 8 byte

2、Java Object Layout

  • 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0):對象的 Mark Word 占用了 8 byte(64 bit),此時存儲的資料為:0x0000000000000001,即無鎖狀态
  • 8 4 (object header: class) 0xf80001e5:對象的 Class Pointer 占用了 4 byte(32 bit),因為開啟了壓縮指針,引用資料類型占用 4 byte
  • 12 4 (object alignment gap):對象的 Padding 占用了 4 byte,因為 Mark Word + Class Pointer = 12 byte,不是 8 byte 的整數,是以需要 4 byte 的對齊填充
  • Instance size: 16 bytes:目前對象占用的記憶體大小,目前對象沒有執行個體資料,也就是一個對象最小占用記憶體大小為 16 byte(64 位虛拟機)

壓縮指針預設是開啟(-XX:+UseCompressedOops)的,當我們關閉壓縮指針(-XX:-UseCompressedOops),重新執行測試程式輸入如下:

JOL:Java 對象記憶體布局

1、VM DESC

  • Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] 和 Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]:關閉壓縮指針後隻有引用資料類型占用記憶體由 4 byte 變為 8 byte,其餘基本資料類型占用記憶體不變

2、Java Object Layout

  • 0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0):較關閉壓縮指針之前無變化
  • 8 8 (object header: class) 0x0000019fd2471c00:Class Pointer 由之前占用 4 byte 變為占用 8 byte,由于此時 Mark Word + Class Pointer = 16 byte,是 8 byte 整數倍,是以不需要對齊填充

測試具有字段的類的代碼如下:(開啟壓縮指針)

public class JolTest {
    public static void main(String[] args) {
        Console.log(ClassLayout.parseInstance(new Demo()).toPrintable());
    }

    public static class Demo {
        private String referenceField;
        private boolean booleanField;
        private byte byteField;
        private short shortField;
        private char charField;
        private int intField;
        private float floatField;
        private long longField;
        private double doubleField;
    }
}           

輸出結果如下:

JOL:Java 對象記憶體布局

繼續閱讀