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