JAVA物件分析之偏向鎖、輕量級鎖、重量級鎖升級過程

等不到的口琴發表於2021-02-03

在HotSpot虛擬機器裡,物件在堆記憶體中的儲存佈局可以劃分為三個部分:

物件頭(Header)

例項資料(Instance Data)

對齊填充(Padding)。

物件頭

HotSpot虛擬機器(後面沒有說明的話預設是這個虛擬機器)物件頭包括三部分:

1、Mark Word

2、指向類的指標

3、陣列長度(只有陣列物件才有)

物件頭之Mark Word

Mark Word記錄了物件和鎖有關的資訊,當這個物件被synchronized關鍵字當成同步鎖時,圍繞這個鎖的一系列操作都和Mark Word有關。

Mark Word在32位JVM中的長度是32bit,在64位JVM中長度是64bit。

Mark Word在不同的鎖狀態下儲存的內容不同,在32位JVM中是這麼存的:

一共32位,兩位用來記錄鎖的資訊,1位用來記錄是否是偏向鎖,如果偏向鎖是1的話,那麼會分配23位來記錄偏向的執行緒id,當計算過Hash後,意味著會分配25bit來記錄HashCode,那麼久沒有空間用來記錄偏向鎖的執行緒ID了,所以計算過HashCode後就沒法再進入偏向鎖。如果進入輕量級鎖或者重量級鎖,意味著會用30bit指向指標,那麼此時物件頭中就只有兩種資訊,鎖標誌、指向鎖的指標。

其中無鎖和偏向鎖的鎖標誌位都是01,只是在前面的1bit區分了這是無鎖狀態還是偏向鎖狀態。

JDK1.6以後的版本在處理同步鎖時存在鎖升級的概念,JVM對於同步鎖的處理是從偏向鎖開始的,隨著競爭越來越激烈,處理方式從偏向鎖升級到輕量級鎖,最終升級到重量級鎖。

結合Mark Word分析鎖升級的流程:

1,當沒有被當成鎖時,這就是一個普通的物件,Mark Word記錄物件的HashCode,鎖標誌位是01,是否偏向鎖那一位是0(0則false , 1 則true)。

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。在這個狀態下,未搶到鎖的執行緒都會被阻塞。

物件頭之指向類的指標

該指標在32位JVM中的長度是32bit,在64位JVM中長度是64bit。

Java物件的類資料儲存在方法區。 並不是所有的虛擬機器實現都必須在物件資料上保留型別指標,換句話說,查詢物件的後設資料資訊並不一定要經過物件本身。

物件頭之陣列長度

如果物件是一個Java陣列,那在物件頭中還必須有一塊用於記錄陣列長度的資料,因為虛擬機器可以通過普通Java物件的後設資料資訊確定Java物件的大小,但是如果陣列的長度是不確定的,將無法通過後設資料中的資訊推斷出陣列的大小。

只有陣列物件儲存了這部分資料, 該資料在32位和64位JVM中長度都是32bit。

例項資料

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

對齊填充

這並不是必然存在的,也沒有特別的含義,它僅僅起著佔位符的作用。由於HotSpot虛擬機器的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,換句話說就是任何物件的大小都必須是8位元組的整數倍。物件頭部分已經被精心設計成正好是8位元組的倍數(1倍或者2倍),因此,如果物件例項資料部分沒有對齊的話,就需要通過對齊填充來補全。

站在巨人的肩膀上

1.<<深入理解Java虛擬機器:JVM高階特性與最佳實踐(第3版) (華章原創精品)>>,周志明

相關文章