JAVA對象布局之對象頭(Object Header)


由於Java面向對象的思想,在JVM中需要大量存儲對象,存儲時為了實現一些額外的功能,需要在對象中添加一些標記字段用於增強對象功能 。在學習並發編程知識synchronized時,我們總是難以理解其實現原理,因為偏向鎖、輕量級鎖、重量級鎖都涉及到對象頭,所以了解java對象頭是我們深入了解synchronized的前提條件,以下我們使用64位JDK示例

1.對象布局的總體結構

2.獲取一個對象布局實例

1.首先在maven項目中 引入查看對象布局的神器

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

2.調用ClassLayout.parseInstance().toPrintable()

public class Main{
    public static void main(String[] args) throws InterruptedException {
        L l = new L();  //new 一個對象 
        System.out.println(ClassLayout.parseInstance(l).toPrintable());//輸出 l對象 的布局
    }
}
//對象類
class L{
    private boolean myboolean = true;
}

運行后輸出:

 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)                           f0 e4 2c 11 (11110000 11100100 00101100 00010001) (288154864)
     12     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     16     1   boolean L.myboolean                               true
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

對象頭所占用的內存大小為16*8bit=128bit。如果大家自己動手去打印輸出,可能得到的結果是96bit,這是因為我關閉了指針壓縮。jdk8版本是默認開啟指針壓縮的,可以通過配置vm參數關閉指針壓縮。關於更多壓縮指針訪問JAVA文檔:官網

關閉指針壓縮		-XX:-UseCompressedOops 

開啟指針壓縮之后,再看對象的內存布局:

 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)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     1   boolean L.myboolean                               true
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
  • OFFSET:偏移地址,單位字節;
  • SIZE:占用的內存大小,單位為字節;
  • TYPE DESCRIPTION:類型描述,其中object header為對象頭;
  • VALUE:對應內存中當前存儲的值;

開啟指針壓縮可以減少對象的內存使用。因此,開啟指針壓縮,理論上來講,大約能節省百分之五十的內存。jdk8及以后版本已經默認開啟指針壓縮,無需配置。

普通的對象獲取到的對象頭結構為:

|--------------------------------------------------------------|
|                     Object Header (128 bits)                 |
|------------------------------------|-------------------------|
|        Mark Word (64 bits)         | Klass pointer (64 bits) |
|------------------------------------|-------------------------|

普通對象壓縮后獲取結構:

|--------------------------------------------------------------|
|                     Object Header (96 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (64 bits)         | Klass pointer (32 bits) |
|------------------------------------|-------------------------|

數組對象獲取到的對象頭結構為:

|---------------------------------------------------------------------------------|
|                                 Object Header (128 bits)                        |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(64bits)       | Klass pointer(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|
 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)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
     12     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
     16    20    int [I.<elements>                             N/A
     36     4        (loss due to the next object alignment)
Instance size: 40 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

3.對象頭的組成

我們先了解一下,一個JAVA對象的存儲結構。在Hotspot虛擬機中,對象在內存中的存儲布局分為 3 塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)
在我們剛剛打印的結果中可以這樣歸類:

 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)    //markword             01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)    //markword             00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)   //klass pointer 類元數據 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     1   boolean L.myboolean                               true	// Instance Data 對象實際的數據
     13     3           (loss due to the next object alignment)			//Padding 對齊填充數據

1.Mark Word

這部分主要用來存儲對象自身的運行時數據,如hashcode、gc分代年齡等。mark word的位長度為JVM的一個Word大小,也就是說32位JVM的Mark word為32位,64位JVM為64位。
為了讓一個字大小存儲更多的信息,JVM將字的最低兩個位設置為標記位,不同標記位下的Mark Word示意如下:

img

其中各部分的含義如下:
lock:2位的鎖狀態標記位,由於希望用盡可能少的二進制位表示盡可能多的信息,所以設置了lock標記。該標記的值不同,整個mark word表示的含義不同。
通過倒數三位數 我們可以判斷出鎖的類型

enum {  locked_value             	= 0, // 0 00 輕量級鎖
         unlocked_value           = 1,// 0 01 無鎖
         monitor_value            = 2,// 0 10 重量級鎖
         marked_value             = 3,// 0 11 gc標志
         biased_lock_pattern      = 5 // 1 01 偏向鎖
  };
通過內存信息分析鎖狀態

寫一個synchronized加鎖的demo分析鎖狀態
接着,我們再看一下,使用synchronized加鎖情況下對象的內存信息,通過對象頭分析鎖狀態。

代碼:

public class Main{
    public static void main(String[] args) throws InterruptedException {
        L l = new L();
        Runnable RUNNABLE = () -> {
            while (!Thread.interrupted()) {
                synchronized (l) {
                    String SPLITE_STR = "===========================================";
                    System.out.println(SPLITE_STR);
                    System.out.println(ClassLayout.parseInstance(l).toPrintable());
                    System.out.println(SPLITE_STR);
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        for (int i = 0; i < 3; i++) {
            new Thread(RUNNABLE).start();
        }
    }
}

class L{
    private boolean myboolean = true;
}

輸出:

===========================================
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           5a 97 02 c1 (01011010 10010111 00000010 11000001) (-1056794790)
      4     4           (object header)                           d7 7f 00 00 (11010111 01111111 00000000 00000000) (32727)
      8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     1   boolean L.myboolean                               true
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

===========================================

Mark Word為0X00007FD7C102975A 對應的2進制為: 0xb00000000 00000000 01111111 11010111 11000001 00000010 10010111 01011010
我們可以看到在第一行object header中 value=5a 對應的2進制為01011010 倒數第三位 為0表示不是偏量鎖,后兩位為10表示為重量鎖

enum {  locked_value             	= 0, // 0 00 輕量級鎖
         unlocked_value           = 1,// 0 01 無鎖
         monitor_value            = 2,// 0 10 重量級鎖
         marked_value             = 3,// 0 11 gc標志
         biased_lock_pattern      = 5 // 1 01 偏向鎖
  };

例子2:

public class Main{
    public static void main(String[] args) throws InterruptedException {
        L l = new L();
        synchronized (l) {
            Thread.sleep(1000);
            System.out.println(ClassLayout.parseInstance(l).toPrintable());
            Thread.sleep(1000);
        }     //輕量鎖
    }
}

class L{
    private boolean myboolean = true;
}

輸出:

 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           f0 18 58 00 (11110000 00011000 01011000 00000000) (5773552)
      4     4           (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     1   boolean L.myboolean                               true
     13     3           (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total

對應的mark word為0x00007000005818f0 對應的2進制為0xb00000000 00000000 01110000 00000000 00000000 01011000 00011000 11110000
根據末尾倒數第三位為0 表示不是偏量鎖 倒數后2位為00 表示這是一個輕量鎖

enum {  locked_value             	= 0, // 0 00 輕量級鎖
         unlocked_value           = 1,// 0 01 無鎖
         monitor_value            = 2,// 0 10 重量級鎖
         marked_value             = 3,// 0 11 gc標志
         biased_lock_pattern      = 5 // 1 01 偏向鎖
  };

你可能會有疑問mark word = 0x00007000005818f0是怎么算出來的, 根據前64位的value倒序排列拼成的串就是mark word
例子:

 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           f0 18 58 00 (11110000 00011000 01011000 00000000) (5773552)
      4     4           (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4           (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     1   boolean L.myboolean                               true
     13     3           (loss due to the next object alignment)

Mark word 串為 前64位倒序排列為:00000000 00000000 01110000 00000000 00000000 01011000 00011000 11110000
轉換為16進制為 00007000005818f0

2.Klass Pointer

即對象指向它的元數據的指針,虛擬機通過這個指針來確定是哪個類的實例。並不是所有的虛擬機實現都必須在對象數據上保留類型指針(通過句柄池訪問)。

簡單引申一下對象的訪問方式,我們創建對象的目的就是為了使用它。所以我們的Java程序在運行時會通過虛擬機棧中本地變量表的reference數據來操作堆上對象。但是reference只是JVM中規范的一個指向對象的引用,那這個引用如何去定位到具體的對象呢?因此,不同的虛擬機可以實現不同的定位方式。主要有兩種:句柄池和直接指針。

2.1 使用句柄訪問

會在堆中開辟一塊內存作為句柄池,句柄中儲存了對象實例數據(屬性值結構體)的內存地址,訪問類型數據的內存地址(類信息,方法類型信息),對象實例數據一般也在heap中開辟,類型數據一般儲存在方法區中。

優點:reference存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要改變。
缺點:增加了一次指針定位的時間開銷。
file

2.2 使用指針訪問

指針訪問方式指reference中直接儲存對象在heap中的內存地址,但對應的類型數據訪問地址需要在實例中存儲。

優點:節省了一次指針定位的開銷。
缺點:在對象被移動時(如進行GC后的內存重新排列),reference本身需要被修改。

file

總結:

通過句柄池訪問的話,對象的類型指針是不需要存在於對象頭中的,但是目前大部分的虛擬機實現都是采用直接指針方式訪問。此外如果對象為JAVA數組的話,那么在對象頭中還會存在一部分數據來標識數組長度,否則JVM可以查看普通對象的元數據信息就可以知道其大小,看數組對象卻不行

3. 對齊填充字節

因為JVM要求java的對象占的內存大小應該是8bit的倍數,所以后面有幾個字節用於把對象的大小補齊至8bit的倍數,就不特別介紹了

4.JVM升級鎖的過程

1,當沒有被當成鎖時,這就是一個普通的對象,Mark Word記錄對象的HashCode,鎖標志位是01,是否偏向鎖那一位是0。

2,當對象被當做同步鎖並有一個線程A搶到了鎖時,鎖標志位還是01,但是否偏向鎖那一位改成1,前23bit記錄搶到鎖的線程id,表示進入偏向鎖狀態。

3,當線程A再次試圖來獲得鎖時,JVM發現同步鎖對象的標志位是01,是否偏向鎖是1,也就是偏向狀態,Mark Word中記錄的線程id就是線程A自己的id,表示線程A已經獲得了這個偏向鎖,可以執行同步鎖的代碼。

4,當線程B試圖獲得這個鎖時,JVM發現同步鎖處於偏向狀態,但是Mark Word中的線程id記錄的不是B,那么線程B會先用CAS操作試圖獲得鎖,這里的獲得鎖操作是有可能成功的,因為線程A一般不會自動釋放偏向鎖。如果搶鎖成功,就把Mark Word里的線程id改為線程B的id,代表線程B獲得了這個偏向鎖,可以執行同步鎖代碼。如果搶鎖失敗,則繼續執行步驟5。

5,偏向鎖狀態搶鎖失敗,代表當前鎖有一定的競爭,偏向鎖將升級為輕量級鎖。JVM會在當前線程的線程棧中開辟一塊單獨的空間,里面保存指向對象鎖Mark Word的指針,同時在對象鎖Mark Word中保存指向這片空間的指針。上述兩個保存操作都是CAS操作,如果保存成功,代表線程搶到了同步鎖,就把Mark Word中的鎖標志位改成00,可以執行同步鎖代碼。如果保存失敗,表示搶鎖失敗,競爭太激烈,繼續執行步驟6。

6,輕量級鎖搶鎖失敗,JVM會使用自旋鎖,自旋鎖不是一個鎖狀態,只是代表不斷的重試,嘗試搶鎖。從JDK1.7開始,自旋鎖默認啟用,自旋次數由JVM決定。如果搶鎖成功則執行同步鎖代碼,如果失敗則繼續執行步驟7。

7,自旋鎖重試之后如果搶鎖依然失敗,同步鎖會升級至重量級鎖,鎖標志位改為10。在這個狀態下,未搶到鎖的線程都會被阻塞。

總結:本章節主要介紹了對象布局包含對象頭,對象實例數據,和對齊數據.並且介紹了對象頭中包含的信息和解析方法
更多內容請持續關注公眾號:java寶典

關注公眾號:java寶典
a


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM