由於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示意如下:
其中各部分的含義如下:
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本身不需要改變。
缺點:增加了一次指針定位的時間開銷。
2.2 使用指針訪問
指針訪問方式指reference中直接儲存對象在heap中的內存地址,但對應的類型數據訪問地址需要在實例中存儲。
優點:節省了一次指針定位的開銷。
缺點:在對象被移動時(如進行GC后的內存重新排列),reference本身需要被修改。
總結:
通過句柄池訪問的話,對象的類型指針是不需要存在於對象頭中的,但是目前大部分的虛擬機實現都是采用直接指針方式訪問。此外如果對象為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寶典