?
Java物件結構

?
一個物件包括三部分:
物件頭
實體資料
對其填充
物件頭:
Mark Word:用于存盤物件自身運行時的資料,如哈希碼(Hash Code),GC分代年齡,鎖狀態標志,偏向執行緒ID、偏向時間戳等資訊,它會根據物件的狀態復用自己的存盤空間,它是實作輕量級鎖和偏向鎖的關鍵,
Klass Pointer:存盤指向方法區物件型別指標
Array Length:如果是陣列,還包括陣列長度
如果物件為非陣列型別,用2字寬存盤物件頭,
如果物件為陣列型別,用3字寬存盤物件頭,
在32位虛擬機中,1字寬等于4位元組,即32bit,在64位虛擬機中,1字寬等于8位元組,即64bit,如下表所示:

?
實體資料:
存放類的屬性資料資訊,包括父類的屬性資訊,如果是陣列的實體部分還包括陣列的長度,這部分記憶體按4位元組對齊;
對齊填充:
不是必須部分,由于虛擬機要求物件起始地址必須是8位元組的整數倍,對齊填充僅僅是為了使位元組對齊,
物件頭結構
Mark Word:
Java物件頭里的Mark Word里默認存盤物件的HashCode、分代年齡和鎖標記位,
32位JVM 的Mark Word的默認存盤結構如下:

?
在運行期間,Mark Word里存盤的資料會隨著鎖標志位的變化而變化,Mark Word可能變 化為存盤以下4種資料

?
完整結構:

?
在64位虛擬機下,Mark Word是64bit大小的,其存盤結構如下:

?
這里我們主要關注這3個部分:鎖狀態、是否偏向鎖、鎖標志位,

鎖標記位(lock):該標記值表示物件鎖的狀態,
是否為偏向鎖(biased_lock):物件是否啟用偏向鎖標記,只占1個二進制位,為1時表示物件啟用偏向鎖,為0時表示物件沒有偏向鎖,
class pointer:
這一部分用于存盤物件的型別指標,該指標指向它的類元資料,JVM通過這個指標確定物件是哪個類的實體,該指標的位長度為JVM的一個字大小,即32位的JVM為32位,64位的JVM為64位,而64位的物件頭有點浪費空間,JVM默認會開啟指標壓縮,所以基本上也是按32位的形式記錄物件頭的,
開啟壓縮指標(-XX:+UseCompressedOops) 關閉壓縮指標(-XX:-UseCompressedOops)
,其中,oop即ordinary object pointer普通物件指標,
array length:
如果物件是一個陣列,那么物件頭還需要有額外的空間用于存盤陣列的長度,這部分資料的長度也隨著JVM架構的不同而不同:32位的JVM上,長度為32位;64位JVM則為64位,64位JVM如果開啟+UseCompressedOops選項,該區域長度也將由64位壓縮至32位,
synchronized鎖
介紹完物件頭,現在我們來介紹synchronized關鍵字,synchronized是物件鎖,鎖狀態變化就體現在上面介紹的Mark Word中的偏向鎖以及鎖標志位,
鎖升級介紹:
我們先介紹下鎖升級程序,
JD6之后分為無鎖,偏向鎖,輕量級鎖,重量級鎖,其中偏向鎖->輕量級鎖->重量級鎖的升級程序不可逆,

偏向鎖:當一個執行緒第一次獲取到鎖之后,再次申請就可以直接取到鎖
核心思想:
一開始無鎖狀態,JVM會默認開啟“匿名”偏向的一個狀態,就是一開始執行緒還未持有鎖的時候,就預先設定一個匿名偏向鎖,等一個執行緒持有鎖之后,就會利用CAS操作將執行緒ID設定到物件的mark word 的高54位上【64位虛擬機】,如果一個執行緒獲得了鎖,那么鎖就進入偏向模式,此時Mark Word的結構也就變為偏向鎖結構,當該執行緒再次請求鎖時,無需再做任何同步操作,即獲取鎖的程序只需要檢查Mark Word的鎖標記位為偏向鎖以及當前執行緒ID等于Mark Word的ThreadID即可,這樣就省去了大量有關鎖申請的操作,
輕量級鎖:沒有多執行緒競爭,但有多個執行緒交替執行,
輕量級鎖是由偏向鎖升級而來,當存在第二個執行緒申請同一個鎖物件時,偏向鎖將會升級為輕量級鎖,Mark Word 的結構也變為輕量級鎖的結構,注意這里的第二個執行緒只是申請鎖,不存在兩個執行緒同時競爭鎖,可以是一前一后地交替執行同步塊,
執行同步代碼塊之前,JVM會在執行緒的堆疊幀中創建一個鎖記錄(Lock Record),并將Mark Word拷貝復制到鎖記錄中,然后嘗試通過CAS操作將Mark Word中的鎖記錄的指標,指向創建的Lock Record,如果成功表示獲取鎖狀態成功,如果失敗,則進入自旋獲取鎖狀態,如果自旋獲取鎖也失敗了,則升級為重量級鎖,也就是把執行緒阻塞起來,等待喚醒,
自旋鎖與自適應自旋鎖
輕量級鎖失敗后,虛擬機為了避免執行緒真實地在作業系統層面掛起,還會進行一項稱為自旋鎖的優化手段,
自旋鎖:許多情況下,當執行緒沒有獲得monitor物件的所有權時,就會進入阻塞,當持有鎖的執行緒釋放了鎖,當前執行緒才可以再去競爭鎖,但是如果按照這樣的規則,就會浪費大量的性能在阻塞和喚醒的切換上,特別是執行緒占用鎖的時間很短的話,
為了避免阻塞和喚醒的切換,在沒有獲得鎖的時候就不進入阻塞,而是不斷地回圈檢測鎖是否被釋放,這就是自旋,在占用鎖的時間短的情況下,自旋鎖表現的性能是很高的,
但是它也存在缺點:如果鎖被其他執行緒長時間占用,一直不釋放CPU,那么自旋的次數就會變多,占用cpu時間變長導致性能變差,當然我們也可以設定自旋鎖的自旋次數,當自旋一定的次數(時間)后就掛起,但是如果設定次數少了或者多了都會導致性能受到影響,所以在JDK1.6引入了自適應性自旋鎖,
自適應自旋鎖:這種相當于是對上面自旋鎖優化方式的進一步優化,它的自旋的次數不再固定,其自旋的次數由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定,
表現是如果此次自旋成功了,很有可能下一次也能成功,于是允許自旋的次數就會更多,反之,如果很少有執行緒能夠自旋成功,很有可能下一次也是失敗,則自旋次數就更少,這樣能最大化利用資源,隨著程式運行和性能監控資訊的不斷完善,虛擬機對鎖的狀況預測會越來越準確,也就變得越來越智能,
重量級鎖:有多執行緒競爭,執行緒獲取不到鎖進入阻塞狀態,
重量級鎖是由輕量級鎖升級而來,當同一時間有多個執行緒競爭鎖時,鎖就會被升級成重量級鎖,此時其申請鎖帶來的開銷也就變大,
鎖升級驗證
鎖升級程序是非常復雜的,很多理論知識很難用實踐驗證,這里我們只驗證鎖狀態的變化程序,也就是Mark Word中鎖標志位的變化,
這里我們參考一個 Maven 依賴 jol(Java Object Layout),這個類提供了工具方法可以列印虛擬機狀態,
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
接下來查看虛擬機資訊,
System.out.println(VM.current().details());
可以看到是64位的jvm,并且開啟了物件指標壓縮和型別指標壓縮,
開啟偏向鎖
我們設計兩個執行緒,執行緒0和執行緒1,分別獲取物件鎖,然后列印物件頭看鎖狀態變化,
先看開啟偏向鎖的情況:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

?
無鎖競爭,偏向鎖->輕量級鎖

?
執行結果:

?
這里先介紹下如何看物件頭狀態,前面說過64位jvm物件頭的Mark Word占8個位元組,所以這里05 00 00 00 00 00 00 00都是Mark Word,由于jvm采用大端模式存盤位元組,將高位位元組存放在低地址,將低位位元組存放在高地址,所以這里對照物件頭表格來看要倒序,即00000101對應這8位,可以看到這時物件處于偏向鎖狀態,

?

?

?

?
分析執行結果:
執行緒0和執行緒1無并發沖突,執行緒0兩次都是獲取的偏向鎖,驗證了前面關于偏向鎖的定義,執行緒1獲取鎖的時候發現當前占有鎖的是執行緒0,于是升級為輕量級鎖,
有鎖競爭,偏向鎖->重量級鎖

?
執行結果:

?

?

?

?
分析執行結果:
執行緒0和執行緒1存在鎖競爭,于是從偏向鎖升級為重量級鎖,
關閉偏向鎖
-XX:-UseBiasedLocking:

?
無鎖競爭,無鎖->輕量級鎖:

?

?

?

?
執行結果分析:
關閉偏向鎖,默認是無鎖狀態,無鎖競爭,從無鎖狀態升級為輕量級鎖,
有鎖競爭,無鎖->重量級鎖:

?

?

?

?
執行結果分析:
關閉偏向鎖,默認是無鎖狀態,有鎖競爭,從無鎖狀態升級為重量級鎖,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/502974.html
標籤:其他
上一篇:Spring(二)-生命周期 + 自動裝配(xml) +自動裝配(注解)
下一篇:行程、執行緒補充與協程相關介紹
