Java 物件頭那點事

竹根七發表於2022-05-11

概覽

image


  • 物件頭
    存放:關於堆物件的佈局、型別、GC狀態、同步狀態和標識雜湊碼的基本資訊。Java物件和vm內部物件都有一個共同的物件頭格式。
    (後面做詳細介紹)

  • 例項資料
    存放:類的資料資訊,父類的資訊,物件欄位屬性資訊。
    如果物件有屬性欄位,則這裡會有資料資訊。如果物件無屬性欄位,則這裡就不會有資料。
    根據欄位型別的不同佔不同的位元組,例如boolean型別佔1個位元組,int型別佔4個位元組等等;

  • 對齊填充
    存放:為了位元組對齊,填充的資料,不是必須的。
    預設情況下,Java虛擬機器堆中物件的起始地址需要對齊至8的倍數。
    假如物件頭大小為12,例項資料大小為5,最近且大於12+5的8的倍數值是24,則對齊補充大小為:24-12-5=7。


為什麼需要物件填充?

欄位記憶體對齊的其中一個原因,是讓欄位只出現在同一CPU的快取行中。如果欄位不是對齊的,那麼就有可能出現跨快取行的欄位。也就是說,該欄位的讀取可能需要替換兩個快取行,而該欄位的儲存也會同時汙染兩個快取行。這兩種情況對程式的執行效率而言都是不利的。其實對其填充的最終目的是為了計算機高效定址。

物件頭

mark word

OpenJDK(JDK8)地址:https://github.com/openjdk/jdk
根據OpenJDK 官方原始碼中MarkOop.hpp檔案中給的註釋介紹,可以大概看出mark word的組成。

MarkOop.hpp中的註釋1:

32 bits:
--------
           hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
           JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
           size:32 ------------------------------------------>| (CMS free block)
           PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

64 bits:
--------
           unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
           JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
           PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
           size:64 ----------------------------------------------------->| (CMS free block)

MarkOop.hpp中的註釋2:

    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
    [0           | epoch | age | 1 | 01]       lock is anonymously biased

  - the two lock bits are used to describe three states: locked/unlocked and monitor.

    [ptr             | 00]  locked             ptr points to real header on stack
    [header      | 0 | 01]  unlocked           regular object header
    [ptr             | 10]  monitor            inflated lock (header is wapped out)
    [ptr             | 11]  marked             used by markSweep to mark an object
                                               not valid at any other time

MarkOop.hpp中的原始碼1:

  enum { age_bits                 = 4,
         lock_bits                = 2,
         biased_lock_bits         = 1,
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2
  };

如圖:
image


MarkOop.hpp中的原始碼2:

  enum { locked_value             = 0,
         unlocked_value           = 1,
         monitor_value            = 2,
         marked_value             = 3,
         biased_lock_pattern      = 5
  };
  • locked_value
    輕量級鎖狀態值,mark word 最後2位為00,轉為10進製為0。
  • unlocked_value
    無鎖狀態值,mark word 最後3位為001,轉為10進製為1。
  • monitor_value
    重量級鎖狀態值,mark word 最後2位為10,轉為10進製為2。
  • marked_value
    mark word 最後2位為11,轉為10進製為3。
    作用比較複雜,
    1:當鎖升級為重量級鎖的過程中,會將markword設定為這個值。
    2:當物件GC時也要使用這個值。

markOop.hpp部分原始碼如下:

  // 僅用於儲存到Lock Record中,用來表示鎖正在使用重量級監視器(輕量級鎖膨脹為重量級鎖之前會這麼做)
  static markOop unused_mark() {
    return (markOop) marked_value;
  }

  // age operations
  markOop set_marked()   { return markOop((value() & ~lock_mask_in_place) | marked_value); }
  markOop set_unmarked() { return markOop((value() & ~lock_mask_in_place) | unlocked_value); }
  • biased_lock_pattern
    偏向鎖狀態值,mark word 最後3位為101,轉為10進製為5。

markOop.cpp中還有以下程式碼,用以判斷當前markword處於哪種鎖狀態:

  // 輕量級鎖
  bool is_locked()   const {
    return (mask_bits(value(), lock_mask_in_place) != unlocked_value);
  }
  // 偏向鎖
  bool is_unlocked() const {
    return (mask_bits(value(), biased_lock_mask_in_place) == unlocked_value);
  }
  // marked
  bool is_marked()   const {
    return (mask_bits(value(), lock_mask_in_place) == marked_value);
  }
  // 無鎖
  bool is_neutral()  const { return (mask_bits(value(), biased_lock_mask_in_place) == unlocked_value); }
  // 膨脹時 markOop 的特殊臨時狀態。 在鎖外檢視標記的程式碼需要考慮到這一點。
  bool is_being_inflated() const { return (value() == 0); }
  // 鎖物件處於升級為重量級鎖的過程中
  static markOop INFLATING() { return (markOop) 0; }

為什麼物件頭分代佔4bit
因為物件經過15次GC就會被放入老年代,而15轉化為二進位制就是1111,幹好佔4bit.


epoch的作用
抄自於:http://www.itqiankun.com/article/bias-lock-epoch-effect

其本質是一個時間戳,代表了偏向鎖的有效性,epoch儲存在可偏向物件的MarkWord中。

①:除了物件中的epoch,物件所屬的類class資訊中,也會儲存一個epoch值。

②:每當遇到一個全域性安全點時(這裡的意思是說批量重偏向沒有完全替代了全域性安全點,全域性安全點是一直存在的),比如要對class C 進行批量再偏向,則首先對 class C中儲存的epoch進行增加操作,得到一個新的epoch_new。

③:然後掃描所有持有 class C 例項的執行緒棧,根據執行緒棧的資訊判斷出該執行緒是否鎖定了該物件,僅將epoch_new的值賦給被鎖定的物件中,也就是現在偏向鎖還在被使用的物件才會被賦值epoch_new。

④:退出安全點後,當有執行緒需要嘗試獲取偏向鎖時,直接檢查 class C 中儲存的 epoch 值是否與目標物件中儲存的 epoch 值相等, 如果不相等,則說明該物件的偏向鎖已經無效了(因為(3)步驟裡面已經說了只有偏向鎖還在被使用的物件才會有epoch_new,這裡不相等的原因是class C裡面的epoch值是epoch_new,而當前物件的epoch裡面的值還是epoch),此時競爭執行緒可以嘗試對此物件重新進行偏向操作。

klass point

後設資料指標class pointer,即指向方法區的instanceKlass例項(虛擬機器通過這個指標來群定這個物件是哪個類的例項)。

oop.hpp中的原始碼:

class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;

length field

該屬性只有陣列物件才有,用以表示陣列的長度。

arrayOop.hpp中有這麼一段註釋:

// The layout of array Oops is:
//
//  markOop
//  Klass*    // 32 bits if compressed but declared 64 in LP64.
//  length    // shares klass memory or allocated after declared fields.

總結

可能面試的時候會被問到這個問題:為什麼一個物件可以當成一把鎖?
這方面可以與上文中提到的物件頭、markword 進行回答即可。

相關文章