從小工到專家的 Java 進階之旅:HotSpot虛擬機對象探秘
今天我們一起看一下HotSpot虛擬機中的對象。
對象的創建(以 new 關鍵字為例)
創建過程
- Java 虛擬機遇到字節碼new指令,首先檢查new指令參數是否能夠在常量池中定位到一個類的符號引用
如果是,繼續下一步
如果否,執行類加載過程
如果是,檢查這個符號引用代表的類是否已經被加載、解析和初始化
如果否,執行類加載
- 虛擬機為新生對象分配內存(對象所需內存大小在類加載完成后即可確定)
- 虛擬機將分配的內存空間(不包括對象頭)初始化為零值。
- 虛擬機對對象進行必要設置,比如設置對象頭信息:
- 對象是哪個類的實例
- 如何找到類的元數據信息
- 對象的哈希碼
- 對象的 GC 分代年齡
- new指令之后會接著執行<init>()方法,按照程序員意愿初始化對象
內存分配
- 內存分配算法:擬機為新生對象分配內存有指針碰撞和空閑列表兩種方式,具體選擇哪種,取決于垃圾收集器是否帶有空間壓縮整理的能力。Serial、ParNew 帶壓縮整理,采用指針碰撞;CMS 基于清除算法,采用空閑列表。
指針碰撞 (Bump The Pointer):堆內存絕對規整,已使用的在一邊,未使用的在另外一遍,中間通過指針作為分界點指示器,分配內存即移動指針。
空閑列表 (Free List):堆內存不規整,已使用與未使用相互交錯,需要維護一個列表,記錄哪些內存塊可用,分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表記錄。
- 線程安全問題:創建對象比較頻繁,需要保證線程安全,避免多個對象分配了相同的內存區域,一般是兩種方式:
同步處理:虛擬機采用CAS+失敗重試方式保證更新操作的原子性
本地線程分配緩沖:把內存分配的動作按照線程劃分在不同空間之中進行,即每個線程在 Java 堆中預先分配一小塊內存,稱為本地線程分配緩沖 (Thread Local Allocation Buffer, TLAB),哪個線程要分配內存,就在哪個線程的本地緩沖區中分配,只有本地緩沖緩沖區用完了,分配新的緩沖區時才需要同步鎖定。是否使用 TLAB,可以通過參數-XX:+/-UseTLAB參數設定。
對象的內存布局
- 對象頭 (Header)
用于存儲對象自身運行時數據:哈希碼 (Hash Code)、GC 分代年齡、鎖狀態標志、線程持有的鎖、偏向線程 ID、偏向時間戳等,長度在 32 位和 64 位虛擬機分別是 32 比特和 64 比特,官方稱為 Mark Word。
類型指針,即對象指向它的類型元數據指針,Java 虛擬機通過這個指針來確定該對象是哪個類的實例。
如果對象是數組,還有一個數據記錄數組長度
- 實例數據 (Instance Data):即程序代碼里面定義的各種類型的字段內容。存儲順序受虛擬機分配策略 (-XX:FieldsAllocationStyle 參數)和字段在 Java 源碼中定義順序影響。HotSpot 虛擬機默認分配順序為 longs/doubles、ints、shorts/charts、bytes/booleans、oops(Ordinary Object Pointers, OOPS),即相同寬度字段被分配到一起存放,在滿足這個前提條件情況下,在父類中定義的變了會出現在子類之前。如果 HotSpot 虛擬機的+XX:CompactFields 參數設置為 true,子類中較窄的變量也允許插入父類變量的空隙中,以節省空間。
- 對齊填充 (Padding):占位符作用。HotSpot 虛擬機的自動內存管理系統要求對象起始地址必須是 8 字節的整數倍。
對象的訪問定位
Java 程序會通過棧上的 reference 數據來操作堆上的具體對象,主流的訪問方式主要有使用句柄和直接指針兩種:
- 使用句柄:Java 堆中將可能會劃分出一塊內存來作為句柄池,reference 中存儲的是對象的句柄地址,句柄中包含了對象實例數據和類型數據各自的具體地址信息。好處是解耦,reference 中存儲的是穩定句柄地址,在對象被移動(垃圾回收等)時只會改變句柄中實例數據指針,而 reference 本身不需要修改。
- 直接指針:Java 堆中的對象布局必須考慮如何放置訪問類型數據的相關信息,reference 中存儲的是對象地址。好處是速度快,節省一次指針定位時間開銷,HotSpot 主要使用直接指針。
使用句柄
直接指針