深入理解 Java 對象的記憶體布局

對於 Java 虛擬機,我們都知道其記憶體區域劃分成:堆、方法區、虛擬機棧等區域。但一個對象在 Java 虛擬機中是怎樣存儲的,相信很少人會比較清楚地了解。Java 對象在 JVM 中的記憶體布局,是我們了解並發編程同步機制的基礎。

在 HotSpot 虛擬機中,對象在記憶體中存儲的布局可以分為 3 塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

對象頭

HotSpot 虛擬機的對象頭包括兩部分資訊,第一部分用於存儲自身運行時的數據,第二部分用於存儲類型指針。

自身運行時數據

對象頭第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等。這部分數據的長度在 32 位和 64 位的虛擬機中分別為 32bit 和 64bit,官方稱它為「Mark Word」。

為了提高虛擬機的空間效率,Mark Word 被設計成非固定的數據結構,從而可以在不同狀態時存儲不同的數據,從而達到節省數據空間的目的。Mark Word 在不同狀態下存儲的內容如下表格所示。

Java對象的記憶體布局

如上表所示,在 32 位的 HotSpot 虛擬機中,如果對象處於未被鎖定(標誌位為 01)的狀態下,那麼 Mark Word 存儲的就是「對象哈希碼、對象分代年齡」。32bit 空間中的 25bit 用於存儲對象哈希碼,4bit 用於存儲對象分代年齡,2bit 用於存儲鎖標誌位,1bit 固定為 0。

類型指針

對象頭第二部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。 另外,如果對象是一個 Java 數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,因為虛擬機可以通過普通 Java 對象的元數據資訊確定 Java 對象的大小,但是從數組的元數據中卻無法確定數組的大小。

實例數據

實例數據部分是對象真正存儲的有效資訊,包括了程式里各個類型的欄位類型,無論是父類繼承下來的,還是子類中定義的。一般來說,父類定義的變數總會出現在子類之前。

對齊填充

對象填充部分並不是必然存在的,也沒有特別的含義,它僅僅起著佔位符的作用。由於 HotSpot VM 的自動記憶體管理系統要求對象起始地址必須是8位元組的整數倍,換句話說,就是對象的大小必須是 8 位元組的整數倍。而對象頭部分正好是 8 位元組的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊時,就需要通過對齊填充來補全。

總結

本篇文章我們介紹了 Java 對象在 JVM 中的記憶體布局,整體可以分為:對象頭、實例數據、對齊填充三個部分。

第一部分的對象頭包括了對象運行時數據和類型指針。其中對象運行時數據包括:哈希碼、GC 分代年齡、鎖狀態標誌等,類型指針指向對象類型元數據,確定對象是哪個類的實例。第二部分是實例數據,是真正存儲的有效資訊,包括各個類型的欄位。第三部分是對齊填充,因為 JVM 要求對象起始地址必須是 8 位元組的整數倍,所以必須有對齊填充來佔位。

深入理解 Java 對象的記憶體布局