Java 對象到底是如何創建的?類加載機制是什么?對象的內存布局和訪問方式有哪些?
對象是 Java 程序運行的核心,而 JVM 的對象管理機制直接影響程序的運行效率和內存管理能力。
在 Java 中,對象的創建過程離不開類的加載與初始化,因此理解類加載的原理和對象的內存布局,是掌握 JVM 性能優化的關鍵。
本章基于類加載機制的深入解析,將詳細講解對象的創建、內存布局、訪問方式及分配策略,幫助你從理論到實踐全面掌握 JVM 對象管理的底層邏輯。
類加載機制概述
類加載是 Java 對象創建的基礎。
JVM 通過類加載器將 .class 文件中的二進制數據加載到內存,并將其轉化為 JVM 可以識別的運行時數據結構。以下是類加載的核心步驟:
類加載的七個階段
根據《Java 虛擬機規范》,類加載分為七個階段:
- 加載 (Loading) :將 .class 文件的二進制數據加載到內存,生成 Class 對象。
- 驗證 (Verification) :校驗 .class 文件的格式和內容是否符合規范,確保安全性。
- 準備 (Preparation) :為靜態變量分配內存并初始化默認值。
- 解析 (Resolution) :將符號引用替換為直接引用。
- 初始化 (Initialization) :執行靜態變量的賦值及靜態代碼塊。
- 使用 (Using) :通過程序調用類的靜態變量或方法。
- 卸載 (Unloading) :釋放類占用的內存資源。
根據 《Java 虛擬機規范》 中的規定,類加載可以分為七個階段,分別為 加載 (Loading)、驗證 (Verification)、準備 (Preparation)、解析 (Resolution)、初始化 (Initialization)、使用 (Using) 和 卸載 (Unloading),其中 驗證、準備 和 解析 三個階段整體又稱為 鏈接 (Linking)。
圖片
類加載就像從藍圖設計到建筑施工的過程:
- 加載階段是獲取藍圖,確保設計的正確性;
- 驗證階段是檢測建筑規范;
- 準備與解析階段是施工基礎;
- 初始化階段是建筑的竣工與驗收。
加載階段主要是使用 "類加載器" 將本地或者遠程網絡中的字節碼文件,通過讀字節流的方式加載到 Java 虛擬機內存中。在加載階段中 Java 虛擬機主要完成以下三件事情:
- ① 通過一個類的全限定名稱來獲取定義此類的二進制字節流。
- ② 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
- ③ 在內存中生成一個代表這個類的 java.lang.Class 對象,作為方法區中這個類的各種數據的訪問入口。
其中常用的類加載器有三種,分別是:
類加載器 | 描述 |
引導類加載器 BootstrapClassLoader | 引導類加載器是使用 C++ 語言實現的,用于加載 Java 中的核心類庫的,一般會加載 |
擴展類加載器 ExtClassLoader | 擴展類加載器主要負責加載 Java 的擴展類庫,一般會加載 |
應用類加載器 AppClassLoader | 應用類加載器是應用程序中默認的類加載器,可以加載 |
對象的內存分配與初始化
當類加載完成后,JVM 開始為新對象分配內存并完成初始化。
對象內存分配
確定分配區域
- 堆分配:大部分對象分配在堆中。
- 棧上分配:通過逃逸分析,局部且生命周期短的對象可分配在棧上。
分配方式
- 指針碰撞:堆內存連續,分配指針向空閑區域移動。
- 空閑列表:堆內存不連續,分配時通過列表找到合適的空閑塊。
對象初始化流程
- JVM 將分配的內存清零(不包括對象頭)。
- 調用對象的構造方法 <init>,完成實例變量初始化。
對象的內存布局
Java 對象在內存中的布局分為三部分:對象頭、實例數據 和 對齊填充。
圖片
對象頭
對象頭包含以下內容:
- Mark Word ,存儲對象的哈希碼、GC 狀態、鎖標志等運行時信息。
- Class Pointer ,指向對象的類元信息,用于確定對象類型。
- 數組長度(僅數組對象) ,數組對象會額外存儲數組長度信息。
對象頭結構示意圖
圖片
對象訪問方式
JVM 提供了兩種對象訪問模式:句柄池 和 直接指針。
句柄池
句柄:如果使用句柄訪問對象,JAVA 堆中將會劃分一塊內存作為句柄池,reference 中存儲的就是對象的句柄地址,句柄中包含對象實例數據與類型數據。
圖片
優點:對象內存地址變化時,只需更新句柄,而無需修改引用。
直接指針
如果使用直接指針訪問,則 reference 存儲對象地址。優點:訪問速度快,少了一次間接訪問。
圖片
對象內存分配策略
JVM 的內存分配策略與垃圾回收機制密切相關。以下是常見的內存分配方式:
- 棧上分配:通過逃逸分析,JVM 可將生命周期短的對象分配在棧上,避免 GC 的參與。
- 新生代與老年代分配:新生代分配,默認分配在 Eden 區;Survivor 區用于存活對象的復制和晉升。生命周期較長或大對象直接分配到老年代。
對象主要分配在新生代的 Eden 區上,如果啟動了本地線程分配緩沖,將按線程優先在 TLAB 上分配。少數情況下也可能直接分配在老年代中,分配的規則并不是百分之百固定的。
圖片
大對象直接進入老年代
虛擬機提供了一個 -XX:PretenureSizeThreshold參數,令大于這個設置值的對象直接在老年代分配,這樣做的目的是避免在 Eden 區和及兩個 Survivor 區之間發生大量的內存復制。
長期存活的對象將進入老年代
如果對象在 Eden 出生并經過第一次 Minor GC 后仍然存活,并且能被 Survivor 容納的話,將被移動到 Survivor 空間中,并且對象年齡設為 1。
對象在 Survivor 空間中每“熬過”一次 Minor GC,年齡就增加 1 歲,當它的年齡到達一定程度(最大為 15 歲),就將會被晉升到老年代。
對象晉升老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 設置。
對象是否能夠晉升到老年代,也不全由-XX:MaxTenuringThreshold 參數控制,如果 Survivor 空間中相同年齡的所有對象大小總和大于 Survivor 空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代。
空間分配擔保
新生代在發生 Minor GC 之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有對象之和(或者歷次晉升老年代對象的平均大小)。
如果這個條件不成立,那么虛擬機將直接進行 Full GC 動作;如果這個條件成立,那么虛擬機就會進行一次 Minor GC 操作,但是這次 Minor GC 是有風險的,因為比較的值是平均值,可能出現極端的情況 —— 大量對象在 Minor GC 后還存活,這時就只好在失敗后重新發起一次 Full GC。
總結
本章深入解析了類加載機制對對象創建的支持,探討了 JVM 的內存布局、訪問方式及分配策略。
通過理解這些底層原理,開發者可以有效優化代碼性能,并在內存問題排查中更加游刃有余。