Java對象在內存中的結構和鎖狀態升級過程


java對象在堆中主要分爲四部分結構, 分別是對象頭MarkWord, 對象指針ClassWord, 實例對象(如果對象是數組的話, 這裏需要再分成兩部分, 多了一個存儲數組長度的數據位), 8字節對齊位. 下面以64位的JVM爲例, 分析內存中對象的各個結構分別存儲什麼信息和作用.

結構 大小 作用
Mark Word 8bytes 用來存儲對象的各種狀態, hash和鎖標記等
Class Word 4 | 8bytes 指向方法區Class信息的指針, 用來確定當前對象是哪個Class的實例和訪問方法
Instance Data - 存儲對象中的實例數據, 和數組的長度
Padding 0 - 7bytes 補齊填充, 如果整個對象在堆上不足8字節倍數的話, 按8字節倍數對齊

Mark Word

Mark Word用來存儲對象的 identity hash code, Thread ID, GC年代, 偏向鎖狀態, 鎖狀態信息. 其中的很多狀態和信息會隨着當前對象的鎖狀態發生變化而變化. 所以接下來就根據鎖的狀態爲主軸, 列出Mark Word的信息變化.

在這裏插入圖片描述

無鎖狀態

當new出對象後, 並且沒有線程鎖定當前對象時. 當前對象就處於無鎖狀態.

  1. identity hashcode

    佔用32bits, identity hashcode會根據物理內存地址來生成hashcode, 保證每一個不同內存對象的hashcode都不一樣. 對象加鎖後, 沒有足夠的空間來存儲hashcode了, 就將hashcode轉移到管程Monitor中維護.

  2. age

    佔用4bits, 代表當前對象此刻被GC的次數. 因爲只有4個bit, 所以最大隻能到15, 默認情況下就是age達到15這個閾值後GC就會將當前對象從年輕代轉移到老年代. 這個age可以根據JVM參數-XX:MaxTenuringThreshold來設置. 絕大部分情況默認都是15次, GC的CMS默認是6次.

  3. biased lock

    佔用1bit, 通過 0 | 1來判斷當前是否爲偏向鎖狀態. 無鎖狀態爲0.

  4. lock

    佔用2bits, 用來區分輕量級鎖, 重量級鎖, GC標記和其他狀態. 無鎖狀態爲01

偏向鎖狀態

當對象在無鎖狀態下, 有一個線程要鎖定當前對象時, 鎖狀態升級到偏向鎖. 偏向鎖在無線程競爭時, 消除同步達到提高效率的目的.

  • hashcode遷移到管程Monitor中管理
  • 將biased lock標記位置爲1
  • 當前要鎖定的線程信息存入到thread標記位中
  • epoch是一個標記位, 初始值是類中epoch的值. 當一個類的對象發生偏向鎖撤銷(當前偏向線程A, A執行完後線程B申請鎖, 就需要撤銷偏向鎖再重偏向線程B)的次數超過閾值(XX:BiasedLockingBulkRebiasThreshold)20後, 會對該類對象的鎖狀態進行批量重偏向, epoch會自增並同步更新所有類對象的Mark Word, 更新後對象中的epoch就和class中的epoch信息不一致了, 這時再有線程申請鎖時, 直接進行重偏向CAS替換thread信息.
  • 當偏向鎖撤銷超過閾值(XX:BiasedLockingBulkRevokeThreshold)40次後, 虛擬機認爲這個類的對象撤銷鎖太頻繁了直接升級所有類對象的偏向鎖鎖爲輕量級鎖.

偏向鎖之所以會叫偏向鎖就是因爲它會保存申請鎖的線程信息, 並且之後處理會偏向於存儲這些信息的線程. 根據一個沒有來源的統計描述絕大多數的鎖大部分情況下都是被一個線程所持有, 並且我們日常中大部分使用的鎖都是可重入鎖. 當同一個線程多次申請當前對象的鎖時(偏向鎖狀態下), cpu只需要判斷一下偏向鎖保存的線程id是否跟正在申請鎖的線程一致, epoch是否和類的epoch保持一致, 如果一致的話就繼續保持偏向鎖的狀態並且不需要做額外的檢查切換工作(偏向鎖加鎖解鎖的過程效率極高). 如果不一致, 就看上個線程是否還存活, 如果線程不在了就撤銷老的偏向鎖進行重偏向. 否則就撤銷偏向鎖升級到輕量級鎖.

輕量級鎖狀態

當有超過一個存活線程向當前對象申請鎖狀態時, 升級爲輕量級鎖. 輕量級鎖在少量線程競爭時, 使用CAS(CAS解析)和自旋等待在用戶態消除同步, 通常比直接使用重量級鎖效率要高.

  • 將lock狀態標記爲00
  • 拷貝Mark Word中的其他數據到持鎖線程的鎖記錄中.
  • 將lock record指針指向持鎖線程的鎖記錄上.

鎖的字節碼級別是由兩個指令組成, 分別是鎖的入口monnitorenter和鎖的結束monitorexit. 當線程進入monnitorenter後, 會在自己的線程的棧幀上建立一個鎖記錄, 並通過CAS機制嘗試將鎖對象的Mark Word中的信息拷貝到自己的棧幀中, 並將ptr_to_lock_record指針指向自己線程棧幀的鎖記錄上. 也標誌了當前對象現在被該線程鎖了. 線程退出同步塊後將Mark Word再通過CAS還給對象頭, 讓其他線程知道現在鎖空閒了.

輕量級鎖也是自旋鎖, 如果鎖的對象頭中沒有Mark Word信息並有一個鎖記錄指針, 那麼其他線程就一直不能獲取到鎖, 線程就會通過執行一個空循環等待. 自旋的過程中線程還是在用戶態下活躍運行, 保證了線程的響應速度, 一有鎖資源立刻就能繼續運行線程. 但是自旋過程會消耗CPU資源. 如果很多線程都在自旋, 或者有線程一直在自旋那麼資源的消耗還是很可觀的. 所以當自旋超過默認10後, 或有更多的線程參與進來則膨脹爲重量級鎖.

重量級鎖狀態

在重量級鎖狀態下, 對象頭中的ptr_to_heavyweight_monitor指針指向管程Monitor對象. 之後線程的鎖分配操作就要從用戶態移交給內核態去處理, 讓cpu通過操作系統級別的互斥量Monitor對象來管理鎖, 系統創建一個等待隊列, 沒獲取到鎖的線程被系統掛起並在隊列中排隊, 不再像自旋鎖那樣不停得消耗額外的資源. 就是因爲有內核態操作, 操作系統級調度, 掛起線程這些很重的操作, 所以叫重量級鎖.

Class Word

ClassWord中存儲的是一個指針, 這裏所佔的空間會根據JVM參數的不同有不同的大小. 在默認沒有開啓指針壓縮參數(-XX:-UseCompressedOops)時, ClassWord佔8個字節, 開啓指針壓縮後佔4個字節. 開啓指針壓縮是爲了減少一些場景中指針的大小, 避免較大的指針在主存和緩存之間移動數據耗費更多的帶寬, 也會在GC時帶來更多的壓力.

ClassWord指向的是方法區的Class信息, 對象可以通過這個指針訪問自己的類信息和方法信息.

Instance Data

數據實例就是存儲對象屬性的空間, 如果當前對象是數組, 那麼要再多出8個字節用來存儲數組的長度.

基礎類型直接按照自己的大小存儲在Instance Data區域中. 對象引用的話存放一個8字節的指針, 指向當前對象所持有的對象. 對象引用的指針在開啓指針壓縮後體積也會改爲佔用4字節.

Padding

爲什麼需要按固定字節數對齊呢? 如果不對齊數據, 處理器從內存中拿到數據後還需要再調整一下才能正確得訪問對象. 對齊後可以犧牲一小部分空間, 來提升對象的訪問效率. 也可以提升內存GC時拷貝內存時的效率.


轉載請註明出處:https://blog.csdn.net/l2show/article/details/103671211

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