前言
Object object = new Object()談談你對這句話的了解?一般而言在JDK8按照預設的情況下,new一個對象占多少記憶體空間。
- 之前讨論過對象其位置存儲在堆中(正常情況:伊甸園→S0/S1→老年代)
- 現在讨論起布局,即對象的構成是什麼?頭體?
對象記憶體布局
在HotSpot虛拟機裡,對象在堆記憶體中的存儲布局可以劃分為三個部分:對象頭(Header)、執行個體資料(Instance Data)和對齊填充(Padding)
1)對象頭
對象頭包含兩部分資料:
- 運作時元數 Mark Word
- 類型指針 Class Pointer
如果對象是數組,則還需記錄數組的長度。如下圖所示:
運作時中繼資料 Mark World
- 存儲HashCode、對象年齡、鎖狀态标志、線程持有的鎖等資訊
- 在64位系統中,Mark Word占了8個位元組,類型指針占了8個位元組【開啟指針壓縮是4位元組】,一共是16(12)個位元組;
- 如果new一個對象,沒有執行個體資料的話,就是16個位元組【預設是開始指針壓縮的-XX:+UseCompressedClassPointers,對齊填充4位元組】
- Mark Word的存儲結構如下圖所示
類型指針 Class Pointer
- 指向方法區的類元資訊(對象模闆),虛拟機通過這個指針來确定對象是哪個類的執行個體
2)執行個體資料
- 是對象真正存儲的有效資訊,即類中定義的各種類型屬性(包括從父類繼承下來的和本身定義的)。
- 執行個體資料存放具有一定規則:相同寬度的字段總是被配置設定在一起;父類中定義的變量會出現在子類之前;如果CompactFields參數為true(預設為true):子類的窄變量可能插入到父類變量的空隙
3)對齊填充
- 虛拟機要求對象起始位址必須是8位元組的整數倍【具體原因這裡就不贅述了,讀者可自行查閱相關資料】。
- 填充資料不是必須存在的,僅僅是為了位元組對齊這部分記憶體按8位元組補充對齊
4)JOL驗證
引入JOL依賴
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
使用JOL檢視Object對象内部細節
使用JOL檢視Customer對象内部細節
public class Customer {
int id;
boolean flag = false;
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new Customer()).toPrintable());
}
}
- 7→填充的位元組數
- 聲明一個Customer的執行個體,隻有一個對象頭的執行個體對象,12位元組(開啟壓縮指針)+ 4位元組[int] + 1位元組[boolean]=17位元組,此時需要對齊填充到24位元組
預設開啟指針壓縮的 -XX:+UseCompressedClassPointers
- 12 + 4(對齊填充) == 一個對象16位元組(不算執行個體資料)
手動關閉指針壓縮 -XX:-UseCompressedClassPointers
- 8 + 8 == 16位元組(不算執行個體資料)
圖示對象的記憶體布局
對象的通路定位
建立對象是為了後續使用該對象,那麼JVM是如何通過棧幀中的對象引用通路到其内部的對象執行個體的呢?
- 通過棧上的reference通路
而reference類型隻是一個指向對象的引用,并沒有定義這個引用應該通過什麼方式去定位、通路到堆中對象的具體位置,而JVM中主流的對象通路方式主要有使用句柄和直接指針兩種方式。
1)句柄通路
Java堆中可能會劃分出一塊記憶體來作為句柄池,而reference中存儲的就是對象的句柄位址,而句柄中包含了對象執行個體資料與類型資料各自具體的位址資訊。
- 優點:reference中存儲穩定句柄位址,對象被移動(垃圾收集時候移動對象很普遍)時會改變句柄中的執行個體資料指針即可,reference本身不需要被修改
- 缺點:需要多占用一些空間
2)直接指針
- HotSpot使用該方式。
使用直接指針通路的話,Java堆中對象的記憶體布局就必須考慮如何放置通路類型資料的相關資訊,reference中存儲的直接就是對象位址,如果隻是通路對象本身的話,就不需要多一次間接通路的開銷。