- Java并發編程深度解析與實戰
- 譚鋒(Mic)
- 3070字
- 2022-05-10 18:39:17
2.4 synchronzied同步鎖標記存儲分析
如果synchronized同步鎖想要實現多線程訪問的互斥性,就必須保證多個線程競爭同一個資源,這個資源有點類似于生活中停車位上的紅綠指示燈,綠燈表示車位閑置可以停車,紅燈表示車位繁忙不能停車。在synchronized中,這個共享資源就是synchronized(lock)中的lock鎖對象。
這就是對象鎖和類鎖能夠影響鎖的作用范圍的原因,如果多個線程訪問多個鎖資源,就不存在競爭關系,也達不到互斥的效果,就像生活中兩個停車位上的兩個紅綠指示燈,此時如果有兩輛車停車,這兩輛車之間就不會有競爭關系。
所以,從這個層面來看,要實現鎖互斥要滿足如下兩個條件。
? 必須競爭同一個共享資源。
? 需要有一個標記來識別當前鎖的狀態是空閑還是繁忙。
第一個條件通過lock鎖對象來實現即可,第二個條件需要有一個地方來存儲搶占鎖的標記,否則當其他線程來搶占資源時,不知道當前是應該正常執行還是應該排隊,實際上,這個鎖標記是存儲在對象頭中的,下面來簡單分析一下對象頭。
2.4.1 揭秘Mark Word的存儲結構
一個Java對象被初始化之后會存儲在堆內存中,那么這個對象在堆內存中存儲了哪些信息呢?
Java對象存儲結構可以分為三個部分:對象頭、實例數據、對齊填充。當我們構建一個Object lock=new Object()對象實例時,這個lock實例最終的存儲結構就對應如圖2-6所示的模型。

圖2-6 對象在內存中的布局模型
下面分別針對對象頭、實例數據、對齊填充的作用和存儲結構進行詳細的說明。
2.4.1.1 對象頭
Java中對象頭由三個部分組成:Mark Word、Klass Pointer、Length。
Mark Word
Mark Word記錄了與對象和鎖相關的信息,當這個對象作為鎖對象來實現synchronized的同步操作時,鎖標記和相關信息都是存儲在Mark Word中的,具體的相關存儲結構如圖2-7所示。

圖2-7 32位系統中Mark Word的存儲結構
在32位系統中,Mark Word的長度是4字節,在64位系統中,Mark Word的長度是8字節,如圖2-8所示。

圖2-8 64位系統中Mark Word的存儲結構
不管在32位還是64位系統中,Mark Word中都會包含GC分代年齡、鎖狀態標記、hashCode、epoch等信息。從圖中可以看到一個鎖狀態的字段,它包含五種狀態分別是無鎖、偏向鎖、輕量級鎖、重量級鎖、GC標記。Mark Word使用2bit來存儲這些鎖狀態,但是我們都知道2bit最多只能表達四種狀態:01、00、10、11,那么第五種狀態如何表達呢?Mark Word額外通過1bit來表達無鎖和偏向鎖,其中0表示無鎖、1表示偏向鎖。
關于不同鎖的狀態,筆者在后續的內容中會詳細說明。
Klass Pointer
Klass Pointer表示指向類的指針,JVM通過這個指針來確定對象具體屬于哪個類的實例。
它的存儲長度根據JVM的位數來決定,在32位的虛擬機中占4字節,在64位的虛擬機中占8字節,但是在JDK 1.8中,由于默認開啟了指針壓縮,所以壓縮后在64位系統中只占4字節。
Length
表示數組長度,只有構建對象數組時才會有數組長度屬性。
2.4.1.2 實例數據
實例數據其實就是類中所有的成員變量,比如,一個對象中包含int、boolean、long等類型的成員變量,這些成員變量就存儲在實例數據中。
實例數據占據的存儲空間是由成員變量的類型決定的,比如boolean占1字節、int占4字節、long占8字節。如果成員變量是引用類型,那么它的數據大小與虛擬機位數和是否開啟壓縮指針有關系。
2.4.1.3 對齊填充
對齊填充本身沒有任何含義,其目的是使得當前對象實例占用的存儲空間是8字節的倍數,所以如果一個對象的字節大小不是8字節的整數倍,會使用對齊填充來達到這一目的。
為什么要通過增加存儲空間來做填充呢?其實,這類的設計基本上都離不開空間換時間的理念。深層次的原因在于減少CPU訪問內存的頻率,從而達到性能提升的效果,對于這部分的分析,筆者會在第3章中詳細說明。
2.4.2 圖解分析對象的實際存儲
為了讓讀者更好地理解對象在內存中的布局,我們使用下面這個程序來進行詳細說明。

從上述代碼中可以看到,在main()方法中定義了MarkWordExample對象實例,并且該對象包含兩個成員變量:id和name。在main()方法運行之后,就會形成如圖2-9所示的存儲結構。

圖2-9 對象在內存中的存儲結構
2.4.3 通過ClassLayout查看對象內存布局
為了更加直觀地看到一個對象的內存布局信息,OpenJDK官方提供了一個JOL(Java Object Layout)工具,使用步驟如下。
第一步,通過maven依賴引入JOL工具。

第二步,創建一個普通對象。

第三步,通過JOL工具打印對象的內存布局。

第四步,運行結果如下。

字段說明:
? OFFSET:偏移地址,單位為字節。
? SIZE:占用的內存大小,單位為字節。
? TYPE DESCRIPTION:類型描述,其中object header為對象頭。
? VALUE:對應內存中當前存儲的值。
上述內容的解讀如下:
? TYPE DESCRIPTION字段對應的部分表示對象頭(object header),一共占12字節,前面的8字節對應的是對象頭中的Mark Word,最后4字節表示類型指針,它只占4字節是因為默認對指針進行了壓縮。
? TYPE DESCRIPTION字段對應的(loss due to the next object alignment)描述部分,表示對齊填充,這里填充了4字節,從而保證最終的內存大小是8字節的整數倍。最終輸出的Instance size: 16 bytes表示當前對象實例占16字節。
由于ClassLayoutExample只是一個空對象定義,因此在打印結果中只有對象頭和對齊填充,沒有實例數據部分。
2.4.3.1 關于壓縮指針
在默認打印的對象內存布局信息中,Klass Pointer被壓縮成4字節,如果我們不希望開啟壓縮指針功能,則可以增加一個JVM參數-XX:-UseCompressedOops。再次運行ClassLayoutExample,得到的結果如下。

從結果來看,Klass Pointer由4字節變成了8字節,而此時該對象的大小正好是16字節,是8字節的整數倍,因此不需要進行填充了。
2.4.3.2 詳述對齊填充的作用
CPU在訪問內存讀取數據時,并不是按照逐個字節來訪問的,而是以字長(Word Size)為單位來訪問的。簡單地說,字長是指CPU一次能夠并行處理的二進制位數,字長總是8字節的整數倍。
比如在64位的操作系統中,CPU訪問內存讀取數據的單位就是8字節,在32位的操作系統中,CPU訪問內存讀取數據的單位是4字節,這樣設計的目的是減少CPU訪問內存的次數,提升CPU的使用率。
假設一個變量在內存中的存儲跨越兩個字長,形成如圖2-10所示的結構,比如一個int類型的變量y占4字節,圖2-8左邊表示未對齊填充的內存布局,它會存在跨字長存儲,右邊表示對齊填充后的內存布局,不存在跨字長存儲的情況。
如圖2-11所示,在未對齊填充的內存布局中,CPU要讀取變量y,由于跨越了兩個字長,所以需要訪問兩次內存,第一次讀取第一個字長獲得最后三個有效字節,第二次讀取第二個字長獲得第二個字長的第一個有效字節,然后在寄存器中進行拼接。

圖2-10 內存布局

圖2-11 未對齊填充的數據讀取方式
但是在對齊填充的內存布局中,CPU讀取變量x或者y,都只需要一次內存訪問,雖然做了無效填充,但是訪問內存的次數減少了,這種方式的計算性能更高,因此本質上來說這就是一種空間換時間的設計方式。
2.4.4 Hotspot虛擬機中對象存儲的源碼
在Hotspot虛擬機中,我們在使用new來創建一個普通對象實例的時候,實際上在JVM層面會創建一個instanceOopDesc對象,而如果對象實例是數組類型,則會創建一個arrayOopDesc對象。instanceOopDesc對象的定義在Hotspot源碼的instanceOop.hpp文件中,arrayOopDesc對象定義在Hotspot源碼的arrayOop.hpp文件中。
當在Java中實例化一個對象時,在JVM中會創建一個instanceOopDesc對象,該對象定義在instanceOopDesc.hpp文件中,核心代碼如下。

instanceOopDesc繼承了oopDesc,oopDesc的定義在oop.hpp文件中,代碼如下。


這種寫法給出了C++中的繼承關系,在普通實例對象中,oopDesc的定義包含兩個成員,分別是_mark和_metadata,Hotspot虛擬機采用OOP-Klass模型來描述Java對象實例,OOP(Ordinary Object Point)指的是普通對象指針,Klass用來描述對象實例的具體類型。
? _mark表示對象標記,屬于markOop類型,也就是前面提到的Mark Word,它記錄了對象和鎖有關的信息。
? _metadata表示類元信息,類元信息存儲的是對象指向它的類元數據(Klass)的首地址。
○ Klass表示普通指針,指向該對象的類元信息,也就是屬于哪一個Class實例。
○ _compressed_klass表示壓縮指針,默認開啟了壓縮指針,在開啟壓縮指針之后,存儲中占用的字節數會被壓縮。
接著我們重點關注markOop這個對象屬性,markOop是一個markOopDesc類型的指針,它的定義在oopsHierarchy.hpp文件中。

在Hotspot中,markOopDesc這個類的定義在markOop.hpp文件中,代碼如下:


實際上,在markOop.hpp文件的注釋中,同樣可以看到Mark Word在32位和64位虛擬機上的存儲布局。

至此,我們從JVM的源碼中完整地驗證了與對象頭相關的存儲信息。
- Learning ROS for Robotics Programming(Second Edition)
- Python計算機視覺編程
- Learning Network Forensics
- JavaScript:Moving to ES2015
- Python完全自學教程
- 微信小程序項目開發實戰
- Learning PHP 7
- 輕松上手2D游戲開發:Unity入門
- R Data Science Essentials
- TypeScript 2.x By Example
- Python編程快速上手2
- Java EE程序設計與開發實踐教程
- Illustrator CS6中文版應用教程(第二版)
- SQL Server 2012數據庫管理與開發(慕課版)
- Scratch 3.0少兒積木式編程(6~10歲)