對象在內存中的布局
1.對象頭
mark word
class pointer(有些地方寫作klass word)
array length(如果常見的對象是數組則有這項,若不是,則不存在這一項)
2.實例數據
3.對齊填充
對象頭
在32位系統中,mark word占4個字節,class pointer占4個字節,因此對象頭共占8個字節
mark word
32位系統中
|-------------------------------------------------------|--------------------| | Mark Word (32 bits) | State |情況 |-------------------------------------------------------|--------------------| | identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal | a.無鎖不可偏向(有hash),b.無鎖可偏向(無hash) |-------------------------------------------------------|--------------------| | thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased | 偏向鎖已偏向 |-------------------------------------------------------|--------------------| | ptr_to_lock_record:30 | lock:2 | Lightweight Locked | 輕量鎖 |-------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked | 重量鎖 |-------------------------------------------------------|--------------------| | | lock:2 | Marked for GC | GC標識 |-------------------------------------------------------|--------------------|
上述其實表示在鎖升級的時候,對象頭中存儲數據布局
biased_lock | lock:2 | 狀態 |
0 | 01 | 無鎖 |
1 | 01 | 偏向鎖 |
00 | 輕量級送 | |
10 | 重量級所 | |
11 | GC標識 |
注:不能單純根據001和101來判斷時候加了偏向鎖,還應該看時候有偏向時的線程ID,101只是表示對象可偏向,可以參看例子
age GC年齡,對象在Survivor區賦值一次,年齡加1,當達到設定閾值或者。。。時,將會晉升到老年代(對象進入老年代,具體請參考),由於只占4 bits所以最大值位15
identity_hashcode 對象標識嗎,調用System.identityHashCode()時,才會設置進入對象頭,對象沒有重寫hashcode方法,則默認使用該值;若對象重寫了hashcode方法,則對象頭中不保存該數據
thread 持有偏向鎖的線程ID
epoch 偏向時間戳
ptr_to_lock_record 指向棧中鎖記錄的指針
ptr_to_heavyweight_monitor 指向管程Monitor的指針
64位內存布局如下,標記為具體一次和32相同
|------------------------------------------------------------------------------|--------------------| | Mark Word (64 bits) | State | |------------------------------------------------------------------------------|--------------------| | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal | |------------------------------------------------------------------------------|--------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased | |------------------------------------------------------------------------------|--------------------| | ptr_to_lock_record:62 | lock:2 | Lightweight Locked | |------------------------------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked | |------------------------------------------------------------------------------|--------------------| | | lock:2 | Marked for GC | |------------------------------------------------------------------------------|--------------------|
class pointer 指向對象類型數據(這部分數據存儲在方法區中)的指針
在32位JVM中占32 bits,在64位JVM中占64 bits。在64位系統中會導致內存的浪費,JVM提供參數+UseCompressedClassPointers(我想看到這個名字,就應該會明白他的用法了吧)。當然,很多地方寫到使用+UseCompressedOops進行控制,其實這個地方的oop是指ordinary object pointer(普通對象指針),因此+UseCompressedOops其實比+UseCompressedClassPointers所包含的范圍廣
設置+UseCompressedOops,哪些信息會被壓縮?
1.對象的全局靜態變量(即類屬性)
2.對象頭信息:64位平台下,原生對象頭大小為16字節,壓縮后為12字節
3.對象的引用類型:64位平台下,引用類型本身大小為8字節,壓縮后為4字節
4.對象數組類型:64位平台下,數組類型本身大小為24字節,壓縮后16字節
哪些信息不會被壓縮?
1.指向非Heap的對象指針
2.局部變量、傳參、返回值、NULL指針
關閉普通對象指針壓縮,即-UseCompressedOops,則類型指針自動設置為-UseCompressedClassPointers,即使開啟類型指針,即設置+UseCompressedClassPointers,該參數也無效
開啟普通對象指針壓縮,即+UseCompressedOops,則類型指針自動設置為+UseCompressedClassPointers。此時可以設置-UseCompressedClassPointers,該參數設置有效
array length 對於數組對象,另外存了下數據的長度,32為JVM上,為32 bits,在64為JVM上,為64bits,開啟普通對象指針壓縮,即+UseCompressedOops,則占32 bits
實例數據
無論是自己定義的,還是從父類繼承的,都需要記錄下來。
對齊填充
並不是必須存在的,Hot Spot虛擬機自動內存管理系統要求對象起始地址必須是8字節的整數倍,換句話說,就是對象的大小必須是8字節的整數倍。
例子驗證(說明,例子是我在64位JVM上運行的結果)
導入JAR文件
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
public class JavaObjectLayoutTest { public static void main(String[] args) { Object o = new Child(); System.out.println(ClassLayout.parseInstance(o).toPrintable()); o.hashCode(); System.out.println(o.hashCode()); System.out.println("對應十六進制表示:"+Integer.toHexString(o.hashCode())); System.out.println(ClassLayout.parseInstance(o).toPrintable()); } } class Parent { Long a =1l; long b =1; } class Child extends Parent{ }
在vm中設置參數-XX:+PrintCommandLineFlags
運行結果:
-XX:InitialHeapSize=265816960 -XX:MaxHeapSize=4253071360 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC Child object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 82 c1 00 f8 (10000010 11000001 00000000 11111000) (-134168190) 12 4 java.lang.Long Parent.a 1 16 8 long Parent.b 1 Instance size: 24 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total 648129364 對應十六進制表示:26a1ab54 Child object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 09 54 ab a1 (00001001 01010100 10101011 10100001) (-1582607351) 4 4 (object header) 26 00 00 00 (00100110 00000000 00000000 00000000) (38) 8 4 (object header) 82 c1 00 f8 (10000010 11000001 00000000 11111000) (-134168190) 12 4 java.lang.Long Parent.a 1 16 8 long Parent.b 1 Instance size: 24 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
查看打印結果,程序運行時默認開啟了對象指針壓縮和類型指針壓縮-XX:+UseCompressedOops -XX:+UseCompressedClassPointers
新建child對象時,從父類繼承的實例對象也會占對象空間,其中根據是否為基本數據類型,基本類型占用字節請參考https://www.cnblogs.com/sxrtb/p/12294979.html,引用類型在開啟指針壓縮(-XX:+UseCompressedOops)時,占用4個字節(關閉時占8個字節)
在未使用hashcode方法時,對象的內存布局為
此處mark word為 00000000_00000000_00000000_00100110_10100001_10101011_01010100_00001001,這個圖里看到的真好相反,這個涉及到“大端存儲和小端存儲”這個知識。我們這看到的倒着存,是應為(計算機這里)用的小端存儲
此時對象為新建狀態,代表鎖狀態的數據占3 bits,為mark word中最后3 bits,為001,
依次,0001表示GC年齡,這個地方我剛剛新建,怎么就有GC呢,這個運行結果說明我這邊的進行過GC操作(自己做測試,可能不是0001,這個是否進行過GC,可以通過-verbose:gc或者加上-XX:+PrintGCDetails就可以觀察到,后續JVM中我也會寫道具體GC參數說明)
使用過hashcode(System.identityHashCode())后,打印hashcode和其對應的十六進制整數,對象的內存布局為
在64為JVM中,hashcode占31 bits,順着上面的凡是,則0100110 10100001 10101011 01010100表示hashcode,通過圖示,也可以看到這數據用十六進制表示,也剛好是26a1ab54
同理,若位數組對象
public class JavaObjectLayoutTest { public static void main(String[] args) { System.out.println(ClassLayout.parseInstance(new B[3]).toPrintable()); } } class B{ }
-XX:InitialHeapSize=265816960 -XX:MaxHeapSize=4253071360 -XX:+PrintCommandLineFlags -XX:+UseBiasedLocking -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC [LB; object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 81 c1 00 f8 (10000001 11000001 00000000 11111000) (-134168191) 12 4 (object header) 03 00 00 00 (00000011 00000000 00000000 00000000) (3) 16 12 B [LB;.<elements> N/A 28 4 (loss due to the next object alignment) Instance size: 32 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
對照着上面的分析,我就不過多說明了。
一般看別人的例子,都會給對象加鎖,如synchronized(o),此時對象都的信息也會改變
public class JavaObjectLayoutTest { public static void main(String[] args) { Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized (o){ System.out.println(ClassLayout.parseInstance(o).toPrintable()); } } }
-XX:InitialHeapSize=265816960 -XX:MaxHeapSize=4253071360 -XX:+PrintCommandLineFlags -XX:+UseBiasedLocking -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9) 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 java.lang.Object object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) b8 f5 1d 03 (10111000 11110101 00011101 00000011) (52295096) 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
從運行結果上來看,這個地方直接加了一個輕量級鎖。
提問:
1.如果我們這個時候使用hashcode()方法,這個肯定可以打印出來,但是根據對象頭信息布局,此時hashcode的位置已經被占用了,那這個答應出來的hashcode時從哪里來的呢?
2.為什么直接加的是輕量級鎖,而不是偏向鎖
我這個地方主要寫的時對象的內存布局,關於鎖的知識我后續會詳細寫,答案引用別人的博客
可以參考https://blog.csdn.net/P19777/article/details/103125545
這個例子也說明了001和101不能直接判斷是否加了偏向鎖
public class A { }
public class JavaObjectLayoutTest { public static void main(String[] args) throws Exception { // 需要sleep一段時間,因為java對於偏向鎖的啟動是在啟動幾秒之后才激活。 // 因為jvm啟動的過程中會有大量的同步塊,且這些同步塊都有競爭,如果一啟動就啟動 // 偏向鎖,會出現很多沒有必要的鎖撤銷 Thread.sleep(5000); A a = new A(); // 未出現任何獲取鎖的時候 System.out.println(ClassLayout.parseInstance(a).toPrintable()); synchronized (a){ // 獲取一次鎖之后 System.out.println(ClassLayout.parseInstance(a).toPrintable()); } // 輸出hashcode System.out.println(a.hashCode()); // 計算了hashcode之后 System.out.println(ClassLayout.parseInstance(a).toPrintable()); synchronized (a){ // 再次獲取鎖 System.out.println(ClassLayout.parseInstance(a).toPrintable()); } } }
-XX:InitialHeapSize=265816960 -XX:MaxHeapSize=4253071360 -XX:+PrintCommandLineFlags -XX:+UseBiasedLocking -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 0d 00 00 00 (00001101 00000000 00000000 00000000) (13) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 0d 28 1b 03 (00001101 00101000 00011011 00000011) (52111373) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 565760380 A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 09 7c d1 b8 (00001001 01111100 11010001 10111000) (-1194230775) 4 4 (object header) 21 00 00 00 (00100001 00000000 00000000 00000000) (33) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total A object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) a0 f3 07 03 (10100000 11110011 00000111 00000011) (50852768) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
運行結果為什么會這樣,可以參考下面(只是參考)
另外補充,對對象加鎖(在開啟偏向鎖的時候,默認開啟,關閉-XX:-UseBiasedLocking),對象沒使用hashcode,則鎖解除后鎖標志位為101;若重寫hashcode()方法,且調用hashcode(),則鎖解除后鎖標志位為101;若沒有重寫hashcode()方法,且調用hashcode(),則鎖解除后鎖標志位為001;
注意:若使用-XX:+UseBiasedLocking,剛開始啟動JVM時,實際上-XX:+UseBiasedLocking暫時還沒有生效,這個時候創建得對象鎖標記位為001,這個時不可偏向的,升級時只能升級為輕量鎖;再使用輕量級鎖的時候,當前棧幀中建立一個名為Lock Record的控件,用戶存儲鎖對象的Mark Word的拷貝,使用CAS操作嘗試將Mark Word更新為Lock Record的指針,若成功,則標識這個對象擁有了該對象的鎖(此次出hashcode,若Lock Record中有hashcode,則輕量級鎖不變;若無,變成重量級鎖);
若-XX:+UseBiasedLocking徹底生效后,創建的對象的鎖標記位置為101,若有一個線程需要加鎖時,這個鎖為偏向鎖(若此時使用hashcode,此時Mark Word中不可能有hashcode,則鎖直接升級為重量級鎖。若是再使用偏向鎖之前,使用hascode,則鎖標記為001,標識不可偏向,則后續直接添加輕量級鎖)
使用hashcode的時,若數據未加鎖,則此時鎖標記位置未001,標識不可偏向。若對象再鎖的時候使用hashcode,而hashcode由不從得知,則需要進行鎖升級。
鎖標記位置未10,也不代表一定添加的重量級鎖。
引用怎樣訪問(找到)對象的
句柄
直接指針(Hot Spot使用這種方式)
優點:reference存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數據指針,而reference本身不需要改變。
缺點:增加了一次指針定位的時間開銷。
優點:節省了一次指針定位的開銷。
缺點:在對象被移動時reference本身需要被修改。
寫到這里就結束了,這里主要寫的時對象的內存布局,如果要了解鎖升級的過程,可以參考https://www.cnblogs.com/ZoHy/p/11313155.html
例題,分析寫new A()和new B()所占用的空間
static class A{ String s = new String(); int i = 0; } static class B{ String s; int i; }
常見對象占用空間(在64為中開啟指針壓縮)
參考:https://www.jianshu.com/p/3d38cba67f8b
https://github.com/dmlloyd/openjdk/blob/jdk/jdk/src/hotspot/share/oops/markOop.hpp
https://www.jianshu.com/p/8580ab50e261
https://blog.csdn.net/P19777/article/details/103125545
https://www.cnblogs.com/ZoHy/p/11313155.html
markOop.hpp
https://github.com/dmlloyd/openjdk/blob/jdk/jdk/src/hotspot/share/oops/markOop.hpp