Java物件記憶體模型

Awecoder發表於2022-01-16

2 Java物件記憶體模型

在HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為3塊區域:物件頭(Header)、 例項資料(Instance Data)和對齊填充(Padding)。

image

在 JVM 中,Java物件儲存在堆中時,由以下三部分組成:

  • 物件頭(object header):包括了關於堆物件的佈局、型別、GC狀態、同步狀態和標識雜湊碼的基本資訊。Java物件和vm內部物件都有一個共同的物件頭格式。
  • 例項資料(Instance Data):主要是存放類的資料資訊,父類的資訊,物件欄位屬性資訊。
  • 對齊填充(Padding):為了位元組對齊,填充的資料,不是必須的。湊齊8位元組的倍數。

2.1 物件頭

物件頭包括兩部分資訊,第一部分用於儲存物件自身的執行時資料(MarkWord), 如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時 間戳等。物件頭的另外一部分是型別指標(Klass Pointer),即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

// oopDesc hotspot物件頭
volatile markOop  _mark;
union _metadata {
    wideKlassOop    _klass;
    narrowOop       _compressed_klass;
} _metadata;

Mark Word在32位JVM中的長度是32bit,在64位JVM中長度是64bit。

image

image

markword組成內容基本一致。

  • 鎖標誌位(lock):區分鎖狀態,11時表示物件待GC回收狀態, 只有最後2位鎖標識(11)有效。
  • biased_lock:是否偏向鎖,由於無鎖和偏向鎖的鎖標識都是 01,沒辦法區分,這裡引入一位的偏向鎖標識位。
  • 分代年齡(age):表示物件被GC的次數,當該次數到達閾值的時候,物件就會轉移到老年代。
  • 物件的hashcode(hash):執行期間呼叫System.identityHashCode()來計算,延遲計算,並把結果賦值到這裡。當物件加鎖後,計算的結果31位不夠表示,在偏向鎖,輕量鎖,重量鎖,hashcode會被轉移到Monitor中
  • 偏向鎖的執行緒ID(JavaThread):偏向模式的時候,當某個執行緒持有物件的時候,物件這裡就會被置為該執行緒的ID。 在後面的操作中,就無需再進行嘗試獲取鎖的動作。
  • epoch:偏向鎖在CAS鎖操作過程中,偏向性標識,表示物件更偏向哪個鎖。
  • ptr_to_lock_record:輕量級鎖狀態下,指向棧中鎖記錄的指標。當鎖獲取是無競爭的時,JVM使用原子操作而不是OS互斥。這種技術稱為輕量級鎖定。在輕量級鎖定的情況下,JVM通過CAS操作在物件的markword中設定指向鎖記錄的指標。
  • ptr_to_heavyweight_monitor:重量級鎖狀態下,指向物件監視器Monitor的指標。如果兩個不同的執行緒同時在同一個物件上競爭,則必須將輕量級鎖定升級到Monitor以管理等待的執行緒。在重量級鎖定的情況下,JVM在物件的ptr_to_heavyweight_monitor設定指向Monitor的指標。

2.2 物件資訊

使用openjdk的jol工具列印物件資訊。引入依賴

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.8</version>
</dependency>

1 無屬性物件

// 列印無屬性物件
System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
// 物件資訊
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

物件資訊分析

image

  • OFFSET:偏移地址,單位位元組;
  • SIZE:佔用的記憶體大小,單位為位元組;
  • TYPE DESCRIPTION:型別描述,其中object header為物件頭;
  • VALUE:對應記憶體中當前儲存的值,二進位制32位;

對於普通無屬性物件,一共佔用16位元組,其中物件頭markword佔用8個位元組,型別指標佔用4個位元組,剩餘4個位元組用於對齊填充,沒有例項資料。

JDK8版本預設開啟指標壓縮(-XX:+UseCompressedOops),如果關閉指標壓縮,型別指標佔用8個位元組,不再需要對齊填充。

2 陣列物件

// 列印陣列
System.out.println(ClassLayout.parseInstance(new int[]{}).toPrintable());
// 物件資訊
[I object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
     12     4        (object header)   // 陣列長度              00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     0    int [I.<elements>                             N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

對於陣列物件,型別指標後有4個位元組記錄陣列長度。因為虛擬機器可以通過普通Java物件的後設資料資訊確定Java物件大小,如果陣列長度不確定,則無法推斷出陣列物件大小。

3 有屬性物件

// 列印有屬性物件
System.out.println(ClassLayout.parseInstance(new User()).toPrintable());
class User {
    int id;			// 4B
    String name;	// 4B 未經型別壓縮為8B
    byte b;			// 1B
    Object o;		// 4B 未經型別壓縮為8B
}
// 物件資訊
com.lzp.java.jvm.memory.User object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
// 物件頭 12位元組
     0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     8     4                    (object header)                           65 cc 00 f8 (01100101 11001100 00000000 11111000) (-134165403)
// 例項資料
     12     4                int User.id                                   0
     16     1               byte User.b                                    0
     17     3                    (alignment/padding gap)   // 對齊               
     20     4   java.lang.String User.name                                 null
     24     4   java.lang.Object User.o                                    null
// 對齊填充
     28     4                    (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total

通過這一節的分析,我們可以比較輕鬆地估計出一個物件的大小,即物件頭+例項資料+物件填充,得到一個8位元組倍數的值。

2.3 其他

1 JVM每次GC,物件分代年齡加1,當年齡增加到15時晉升到老年代。為什麼是15?

在Mark Word中可以發現標記物件分代年齡的分配的空間是4bit,而4bit能表示的最大數就是2^4-1 = 15。

2 為什麼要進行指標壓縮?

通過指標壓縮,型別指標、物件引用等由8位元組轉為4個位元組。降低物件佔用的記憶體大小,順便減輕GC壓力;當指標移動時,減少頻寬損耗。

相關文章