1. 程式人生 > 其它 >從理論到實踐,刨根問底探索Java物件記憶體佈局

從理論到實踐,刨根問底探索Java物件記憶體佈局

從理論到實踐,刨根問底探索Java物件記憶體佈局

所謂物件的記憶體佈局,就是物件在分配到記憶體中後的儲存格式。

物件在記憶體中的佈局一共包含三部分:

  1. 物件頭(Header)
  2. 例項資料(Instance Data)
  3. 對齊填充(Padding)

第一部分:物件頭

首先來看一下物件頭的結構

Java物件頭分為兩部分:

  1. Mark Word:物件自身執行時資料。
  2. Klass Pointer:型別指標,即物件指向它的類元資料的指標。

1、Mark Word

為啥叫Mark Word呢?我理解因為這部分是用來標記物件執行時的資料和狀態,比如物件的HashCode、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID等。而Word呢,是因為這段資訊是用一個Word(字)長度來儲存的。在32位系統中,一個字是32bit,也就是4位元組。64位系統中,一個字是64bit,也就是8位元組。

對於這部分的描述,從markOop.hpp原始碼的註釋中即可得知。

下面就以32位的虛擬機器為例,來探尋一下物件頭的Mark Word部分是什麼樣的資料結構。

先劃重點,鎖狀態很重要

這裡要注意兩點:

  1. 物件頭的資料格式和物件的鎖狀態緊密相關。在不同的鎖狀態下,物件頭的結構都不一樣。其目的是為了儘量在極小的空間記憶體儲儘量多的資訊。
  2. 鎖狀態的標誌位是固定的,無論是32位還是64位的虛擬機器,物件頭中最後兩位就是鎖的狀態標誌。

既然鎖的狀態很重要,那麼就先看一下下鎖標誌對應的狀態含義:

lock 狀態
01 無鎖
00 輕量級鎖(locked)
10 重量級鎖(monitor,inflated lock)
11 GC標記(marked)

01-無鎖狀態下的Mark Word結構

無鎖狀態下,涉及到兩種情況:

  1. 稀鬆平常的無鎖狀態
  2. 偏向鎖

是否存在偏向鎖,我們也是用1位長的標識來判斷。

當不存在偏向鎖時,1-25位是物件的HashCode,之後的4位是物件GC的分代年齡,之後的1位是偏向鎖的標誌,此時該標誌為0。

當存在偏向鎖時,1-23為是持有偏向鎖的執行緒的ID,之後的2位是偏向時間戳,然後4位依舊是物件GC的分代年齡,再之後的1位是偏向鎖的標誌,此時該標誌為1。

00-輕量級鎖狀態下的Mark Word結構

輕量級鎖狀態時,物件頭的前30位儲存指向持有鎖的執行緒的棧幀中鎖記錄的指標。

此時,獲取了該物件偏向鎖的執行緒,會線上程的棧幀上建立鎖記錄的空間,並通過CAS的方式將物件頭的資訊複製到鎖記錄的位置,並將物件頭替換成指向鎖記錄的指標。

10-重量級鎖狀態下的Mark Word結構

當有兩個及以上的執行緒競爭同一個鎖,則輕量級鎖就會升級成重量級鎖。此時物件頭的前30位儲存的是指向重量級鎖Monitor的指標。

關於Monitor,這裡可以做個簡單的理解:Java的重量級鎖,是通過一個Monitor物件來實現的。JVM通過Monitor物件中的_owner、_EntryList來維護是哪個執行緒持有這個物件的鎖,以及後續的阻塞執行緒。原始碼在objectWaiter.hpp中可以深入瞭解。

11-GC標記

當最後兩位為11時,代表被GC標記了,則物件頭前面的30位資訊為空。

Mark Word 小結

在這裡根據上面的描述,畫個圖來展示一下Mark Word這部分資料在32位系統和62位系統裡的佈局,更直觀清晰一些。

2、Klass Pointer(型別指標)

緊跟著Mark Word,是物件頭的另一部分——型別指標。型別指標也是用一個字的長度(32位系統是4byte,64位系統是8byte)來儲存的。這個指標會指向該物件對應的類元資料,說人話就是,JVM通過這個指標知道這個物件是哪個類的例項。

3、陣列長度

如果這是一個普通的Java物件,則物件頭中只有Mark Word和Klass Pointer兩部分。當它是一個數組物件時,物件頭中還需要一部分空間來儲存陣列的長度。有了陣列長度,JVM才能夠知道一個數組物件的大小。陣列長度這部分也是用一個字的長度(32位系統是4byte,64位系統是8byte)來儲存。

如果64位的JVM開啟了+UseCompressedOops選項,則型別指標和陣列長度這兩個區域都會被壓縮成32位。

第二部分:例項資料

這部分就是物件真正儲存的有效資訊,也就是類裡定義的各種型別欄位的內容。

第三部分:對齊填充

這部分沒有實際的含義,僅僅是起到佔位符的作用。因為JVM要求物件起始地址必須是8位元組的整數倍,也就是一個物件的大小必須是8位元組的整數倍,所以如果一個物件的例項資料不滿足8位元組的整數倍,則需要做一個對齊填充的操作,保證物件的大小是8位元組的整數倍。

實踐一下

接下來我們來跑幾個demo看看真實的物件頭佈局,藉助JOL(Java Object Layout),我們可以分析JVM中物件的佈局。

環境:64位系統,JDK8。

1、引入JOL依賴

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

2、一個簡單的類

public class OneObject {
    private int id;
    private String name;
    private double score;
}

3、列印物件記憶體佈局

這裡選擇了五種情況:

  1. 剛剛new出來的新鮮物件
  2. 經歷過gc後的物件
  3. 加鎖時的物件(偏向鎖)
  4. 多執行緒競爭鎖時的物件(重量級鎖)
  5. 算一下物件的hashCode

程式碼如下:

@Test
public void showObjectData() throws InterruptedException {
    OneObject object = new OneObject();
    log.info("初始化後的物件佈局:{}", ClassLayout.parseInstance(object).toPrintable());

    System.gc();
    log.info("gc一次之後的物件佈局:{}", ClassLayout.parseInstance(object).toPrintable());

    synchronized (object) {
        log.info("加鎖時的物件佈局:{}", ClassLayout.parseInstance(object).toPrintable());
    }

    for (int i = 0; i < 2; i++) {
        Thread thread = new Thread(()->{
            synchronized (object) {
                log.info("競爭鎖時的物件佈局:{}", ClassLayout.parseInstance(object).toPrintable());
            }
        });
        thread.start();
    }

    Thread.sleep(500);
    object.hashCode();
    log.info("計算完hashCode的物件佈局:{}", ClassLayout.parseInstance(object).toPrintable());
}

4、輸出結果

執行以上的程式碼,輸出結果如下:

00:06:03.877 [main] INFO com.esparks.pandora.learning.vm.LearnObjectData - 初始化後的物件佈局:com.esparks.pandora.learning.vm.OneObject object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)
     12     4                int OneObject.id                              0
     16     8             double OneObject.score                           0.0
     24     4   java.lang.String OneObject.name                            null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

00:06:03.890 [main] INFO com.esparks.pandora.learning.vm.LearnObjectData - gc一次之後的物件佈局:com.esparks.pandora.learning.vm.OneObject object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)
     12     4                int OneObject.id                              0
     16     8             double OneObject.score                           0.0
     24     4   java.lang.String OneObject.name                            null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

00:06:03.891 [main] INFO com.esparks.pandora.learning.vm.LearnObjectData - 加鎖時的物件佈局:com.esparks.pandora.learning.vm.OneObject object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           90 99 0b 6d (10010000 10011001 00001011 01101101) (1829476752)
      4     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      8     4                    (object header)                           38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)
     12     4                int OneObject.id                              0
     16     8             double OneObject.score                           0.0
     24     4   java.lang.String OneObject.name                            null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

00:06:03.892 [Thread-1] INFO com.esparks.pandora.learning.vm.LearnObjectData - 競爭鎖時的物件佈局:com.esparks.pandora.learning.vm.OneObject object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           62 d9 00 20 (01100010 11011001 00000000 00100000) (536926562)
      4     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      8     4                    (object header)                           38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)
     12     4                int OneObject.id                              0
     16     8             double OneObject.score                           0.0
     24     4   java.lang.String OneObject.name                            null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

00:06:03.893 [Thread-2] INFO com.esparks.pandora.learning.vm.LearnObjectData - 競爭鎖時的物件佈局:com.esparks.pandora.learning.vm.OneObject object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           62 d9 00 20 (01100010 11011001 00000000 00100000) (536926562)
      4     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      8     4                    (object header)                           38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)
     12     4                int OneObject.id                              0
     16     8             double OneObject.score                           0.0
     24     4   java.lang.String OneObject.name                            null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

00:06:04.398 [main] INFO com.esparks.pandora.learning.vm.LearnObjectData - 計算完hashCode的物件佈局:com.esparks.pandora.learning.vm.OneObject object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           09 63 c1 aa (00001001 01100011 11000001 10101010) (-1430166775)
      4     4                    (object header)                           56 00 00 00 (01010110 00000000 00000000 00000000) (86)
      8     4                    (object header)                           38 a1 08 00 (00111000 10100001 00001000 00000000) (565560)
     12     4                int OneObject.id                              0
     16     8             double OneObject.score                           0.0
     24     4   java.lang.String OneObject.name                            null
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0

物件佈局分析

這裡我們先以第一種情況的物件佈局來簡單說明輸出的格式,直接在圖上標明啦。

這裡要插一句,JOL在輸出物件頭的時候,是按照四個位元組的長度從記憶體中獲取物件的物件頭資料。所以你會看到64位的Mark Word會拆成兩行(兩個4位元組)打印出來。

幾個問題

看到這裡的時候,腦海裡不禁冒出幾個問題。

Q1:為什麼Klass pointer只有四個位元組呢?

因為JVM預設開啟了+UseCompressedOops選項,所以Klass Pointer被壓縮成了32位。如果在啟動時配置了-UseCompressedOops選項,那麼Klass Pointer就也是64位啦。

Q2:說好了Mark Word的最後兩位是鎖狀態,這剛建立的物件,最後兩位怎麼就是00了呢?

這個就要和位元組儲存的大小端模式有關了。

舉個例子,一個16進位制的整數0x12345678,對應的二進位制整數為:00010010 00110100 01010110 01111000(12 34 56 78),一共佔用四個位元組。那麼在記憶體中應該如何儲存這長度為4byte的位元組序列的資料呢?

有兩種方式:

  1. 按照記憶體地址的順序,依次儲存12 34 56 78這四個位元組的資料。這種將位元組序列的高序位元組儲存在記憶體的起始地址上的方式,叫大端模式。
  2. 按照記憶體地址的順序,依次儲存78 56 34 12這四個位元組的資料。這種將位元組序列的低序位元組儲存在記憶體的起始地址上的方式,叫小端模式。

而一般我們用的x86或者ARM的CPU,採用的都是小端模式來儲存記憶體中的位元組序列,所以和我們常見的順序是反著的。因此,你看到的輸出的前兩行的物件頭,實際上的值是這樣的:

所以物件初始化後,就是無鎖狀態啦。

分析不同狀態時的物件頭

剛才已經就剛初始化的物件分析過一次記憶體佈局了。而在鎖狀態不同的情況下,變化也只限於物件頭中Mark Word的值變動,所以這裡就快速的分析一下其餘的四種狀態時的Mark Word了。這裡我也自動將輸出的小端格式轉換成正常的順序來分析。也是通過實際情況來回顧驗證一下剛才的理論知識啦,對照前面的圖片中不同的顏色比對就可以了。

1.gc一次之後的Mark Word

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00001001

紅色的01代表無鎖狀態,藍色的0代表無偏向鎖,黃色的0001是GC分代年齡。因為GC過一次,該物件沒有被回收,年齡加1。

2.加鎖時的Mark Word

00000000 00000000 00000000 00000001 01101101 00001011 10011001 10010000

紅色的00代表輕量級鎖狀態,綠色的一串是指向持有鎖的執行緒的棧幀中鎖記錄的指標。

3.競爭鎖時的Mark Word

00000000 00000000 00000000 00000001 00100000 00000000 11011001 01100010

紅色的10代表重量級鎖狀態,綠色的一串是指向重量級鎖Monitor的指標。

4.計算完hashCode的Mark Word

00000000 00000000 00000000 01010110 10101010 11000001 01100011 00001001

紅色的01代表此時迴歸到無鎖狀態,,藍色的0代表無偏向鎖,黃色的0001是GC分代年齡為1,紫色這一串是剛才計算的hashCode,儲存在了這裡。所以只有在呼叫了hashCode()方法時,JVM才會把物件的hashCode儲存到物件頭中。

總結

好啦,總結一下。

本篇文章先是介紹了Java物件的記憶體佈局(由物件頭、例項資料、對齊填充三部分組成);之後詳細的介紹了物件頭的資料結構(Mark Word、Klass Pointer、陣列長度),以及不同鎖狀態下(01無鎖、00輕量級鎖、10重量級鎖、11GC標記),Mark Word中的資料格式以及代表的含義;最後通過JOL打印出物件的記憶體佈局,進一步驗證了前半部分枯燥的理論知識。

希望看到這裡,能讓你徹底的理解Java物件在記憶體中的完整樣貌啦~

參考資料

  1. markOop.hpp原始碼,主要在註釋中
  2. objectWaiter.hpp原始碼
  3. 《深入理解Java虛擬機器》