JVM--Java對象內存佈局

1.Java對象內存佈局

在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例數據、對其數據。如下圖所示:

長度

內容

說明

32/64 bit

Mark Word

存儲對象的HashCode或者鎖信息等

32/64bit

Class Metadata Address

存儲對象類型數據的指針

32/64bit

Array Length

數據的長度(如果當前對象是數組)

對象的存儲佈局

  1. 實例數據:存放類的屬性數據信息,包括父類的屬性信息;

  2. 對齊填充:由於虛擬機要求,對象起始地址必須是8字節的整數倍,填充數據不是必須存在的,僅僅是爲了字節對齊。

  3. 對象頭:Java對象頭一般佔2個機器碼(在32位機器中,1個字節碼等於4個字節,也就是32bit,在64位虛擬機中,1個字節碼是8個字節,也就是64bit)。但是如果對象是數組對象,還需要三個字節碼,因爲JVM虛擬機可以通過Java對象的元數據信息確定Java對象的大小,但是無法從數組的元數據來確認數據的大小,所以用一塊來記錄數組的長度。

2.對象頭

synchronized用的鎖就是存在Java對象頭裏的,那麼什麼是Java對象頭?Hotspot虛擬機的對象頭主要包括兩部分數據:Mark Word(標記字段)、Class Pointer(類型指針)。其中ClassPointer是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是那個類的實例,Mark Word用於存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵。Java對象頭具體結構描述如下:

長度

內容

說明

32/64 bit

Mark Word

存儲對象的HashCode或者鎖信息等

32/64bit

Class Metadata Address

存儲對象類型數據的指針

32/64bit

Array Length

數據的長度(如果當前對象是數組)

Java對象頭結構組成

Mark Word用於存儲對象自身的運行時數據,如哈系碼(HashCode)、GC分代年齡、鎖狀態標識、線程持有的鎖、偏向線程ID、偏向時間戳等。比如鎖膨脹就是藉助Mark Word的偏向的線程ID,參考 Java鎖的膨脹過程和優化。下圖是Java對象頭無鎖狀態下Mark Word 部份的存儲結構(32位虛擬機)。

 

25bit

4bit

1bit是否偏向鎖

2bit鎖標誌位

無鎖狀態

對象的HashCOde

對象分代年齡

0

01

Mark Word存儲結構

對象頭信息是對象自身定義的數據無關的額外存儲成本,但是考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在績效的空間內存存儲儘量多的數據,他會根據對象狀態複用自己的存儲空間,也就是說,MarkWord會隨着程序的運行發生變化,可能變化爲以下4中數據:

鎖狀態

25bit

4bit

1bit

2bit

23bit

2bit

是否偏向

鎖標誌位

輕量級鎖

指向棧中鎖記錄的指針

00

重量級鎖

指向互斥量(重量級鎖)的指針

10

GC標記

11

偏向鎖

線程ID

Epoch

對象粉黛年來

1

01

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

鎖狀態

25bit

31bit

1bit

4bit

1bit

2bit

 

 

 

cms_free

分代年齡

偏向鎖

鎖標誌位

無鎖

unuserd

hashcode

 

 

0

01

偏向鎖

ThreadID(54bit)、Epoch(2bit)

 

 

0

01

 

對象頭的最後兩位存儲了鎖的標誌位,01是初始狀態。未加鎖,其對象頭裏存儲的是對象本身的哈系碼,隨着鎖級別的不通,對象頭會存儲不同的內容。偏向鎖存儲的是當前佔用此對象的線程ID;而輕量級鎖存儲指向線程棧中鎖記錄的指針。從這裏我們可以看到。“鎖”這個東西,可能是個鎖記錄+對象頭裏引用指針(判斷線程是否擁有鎖時將線程的鎖記錄地址和對象頭裏的指針地址進行比較),也可能是對象頭裏的線程ID(判斷線程是否擁有鎖時將線程ID和對象頭裏存儲的現層ID進行比較)。

存儲內容

標誌位

狀態

對象哈系碼、對象分代年齡

01

未鎖定

指向鎖記錄的指針

00

輕量級鎖定

指向重量級鎖的指針

10

碰撞(重量級鎖定)

空,不需要記錄信息

11

GC標記

偏向線程ID、偏向時間戳、對象分代年齡

01

可偏向

 

3.對象頭中Mark Word 與線程中Lock Record

在線程進入同步代碼塊的時候,如果此同步對象沒有被所動,即他的鎖標識位是01,則虛擬機 首先在當前線程的棧中創建我們稱之爲“鎖記錄(Lock Record)” 的空間,用於存儲對象的Mark Word的拷貝,官方把這個拷貝成爲Displaced Mark Word。整個Mark Word及其拷貝至關重要。---置換標記字

Lock Record是線程私有的數據結構,每一個線程都有一個可用的Lock Record列表同時還有一個全局可用列表。每一個被鎖住的對象Mark Word都會和一個Lock Record關聯(對象頭的MarkWord 中的Lock Word指向Lock Record的起始地址 ),同時Lock Record中有一個Owner字段存放該鎖的線程唯一標識(或者object Mark Word),表示該鎖被這個線程佔用。Lock Record內部結構如下圖表。

Lock Record

描述

Owner

初始化時爲null 表示當前沒有任何線程擁有該 monitor Record,當線程陳工擁有該鎖後保存線程唯一標識,當鎖釋放時又設置爲null

EntryQ

關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住的monitor record失敗的線程

RcThis

表示blocked或者waiting在該monitor record上的所有線程的個數

Nest

用來實現重入鎖的計數

HashCode

保存從對象拷貝過來的hashcode值(可能還包含GC age)。

Candidate

用來避免不必要的阻塞或等待線程喚醒,因爲每次只有一個線程能夠成功擁有鎖,如果每次前一個釋放的線程喚醒所有正在阻塞或者等待的線程,會引起不必要的上線爲切換(從阻塞到就緒然後因爲競爭鎖失敗又被阻塞)從而導致性能嚴重下降。Candidate只有兩種可能的值,0表示沒有需要喚醒的線程,1表示要喚醒一個繼任線程來競爭鎖。

 

4.監視器(monitor)

任何一個對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。synchronized在JVM裏的實現都是基於進入和退出monitor對象來實現方法同步和代碼塊同步,雖然具體實現細節不一樣,但是都可以通過成對的monitorentr 和monitorexit指令來實現。

  1. monitorEnter指令:插入在同步代碼塊的開始位置,當代碼執行到該指令時,將會嘗試獲取該對象monitor的所有權,即嘗試獲得該對象的鎖。

  2. monitorExit指令:插入在方法結束處和異常處,JVM保證每個monitorEnter必須有對應的MonitorExit

那什麼是monitor?可以把它理解爲一個同步工具,也可以描述爲一種同步機制,它通常被描述爲一個對象。與一切皆對象一樣,所有的Java對象是天生的monitor,每一個Java對象都有成爲Monitor的潛質,因爲在Java的設計中,每一個Java對象自打孃胎裏出來就帶了一把看不見的鎖,它叫做內部鎖或者Monitor鎖。也就是通常說synchronized的對象鎖,Mark Word鎖標示位爲10 ,其中指針指向的是Monitor對象的起始地址。在Java虛擬機(HotSpot)中,Monitor是有ObjectMonitor實現的,其主要數據結構如下(位於HotSpot虛擬機源碼ObjectMonitor.php文件中,C++實現的):

代碼塊

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

 

ObjectMonitor中有兩個隊列,_WaitSet 和_EntryList,用來保存ObjectWaiter對象列表(每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段代碼時:

  1. 首先會進入_EntryList集合,當線程獲取到對象的monitor後,進入_owner區域並把monitor中的owner變量設置爲當前線程,同時monitor中的計數器count加1;

  2. 若線程調用wait()方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入WaitSet集合中等待被喚醒;

  3. 若當前線程執行完畢,也將釋放monitor(鎖)並復位count的值,以便其他線程進入獲取monitor(鎖);

同時,monitor對象存儲在於每個Java對象的對象頭Mark Word中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖,也是爲什麼Java中任意對象可以作爲鎖的原因,同時notify/notifyAll/wait等方法會使用到Monitor鎖對象,所以必須在同步代碼塊中使用。

監視器monitor有兩種同步方式:互斥與協作。多線程環境下線程之間如果需要共享數據,需要解決互斥訪問數據的問題,監視器可以確保監視器上的數據在同一時刻只會有一個線程在訪問。

什麼時候需要協助?比如:

一個線程向緩衝區寫數據,另一個線程從緩衝區讀數據,如果讀線程發現緩衝區爲空就會等待,當寫線程向緩衝區寫入數據,就會喚醒讀線程,這裏讀線程和寫線程就是一個合作關係。JVM通過Object類的wait方法來使自己等待,在調用wait方法後,該線程會釋放它持有的監視器,直到其他線程通知它纔有執行的機會。一個線程調用notify方法通知在等待的線程,這個等待線程並不會馬上執行,而是要通知線程釋放監視器後,它重新獲取監視器纔有執行的機會。如果剛好喚醒的這個線程需要的監視器被其他線程搶佔,那麼這個線程會繼續等待。Object類中的notifyAll方法可以解決這個問題,它可以喚醒所有等待的線程,總有一個線程執行。

 

如上圖所示,一個線程通過1號門進入Entry Set(入口區),如果在入口區沒有線程等待,那麼這個線程就會獲取監視器成爲監視器的Owner,然後執行監視器區域的代碼。如果在入口區中有其他等待的線程,那麼新來的線程也會和這些線程一起等待。線程在持有監視器的過程中,有兩個選擇,一個是正常執行監視器區域的代碼,釋放監視器,通過5號門退出監視器;還有可能等待某個條件的出現,於是它會通過3號門到Wait Set(等待區)休息,直到相應的條件滿足後再通過4號門重新獲取監視器再執行。

注意:

當一個線程釋放監視器時,在入口區和等待去的等待線程都會區競爭監視器。如果入口區的線程贏了,從從2號門進入;如果等待區的線程贏了會從4號門進入。只有通過3號門才能進入等待去,在等待區中的線程只有通過4號門才能退出等待去,也就是一個線程只有在持有監視器時才能執行wait操作,處於等待的線程只有再次獲取監視器才能退出等待狀態。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章