Java對象內存模型

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壓力;當指針移動時,減少帶寬損耗。

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