2 Java物件記憶體模型
在HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為3塊區域:物件頭(Header)、 例項資料(Instance Data)和對齊填充(Padding)。
在 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。
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
物件資訊分析
- 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壓力;當指標移動時,減少頻寬損耗。