????????在 Java 開發中,自動內存管理是 JVM 的核心能力之一,而內存分配與回收的策略直接影響程序的性能和穩定性。本文將詳細解析 JVM 的內存分配機制、對象回收規則以及背后的設計思想,幫助開發者更好地理解 JVM 的 "自動化" 內存管理邏輯。
一、內存分配的核心原則
????????JVM 的內存分配遵循 "分代思想",即根據對象的存活周期將堆內存劃分為新生代和老年代,并針對不同區域采用不同的分配策略。
1. 對象優先在 Eden 區分配
????????大多數情況下,新創建的對象會首先被分配到新生代的 Eden 區。當 Eden 區沒有足夠空間時,JVM 會觸發一次 Minor GC(新生代垃圾回收)。
public class GCTest {public static void main(String[] args) {// 分配30MB對象(大于Eden區默認大小)byte[] allocation1 = new byte[30900*1024];}
}
????????通過-XX:+PrintGCDetails
參數運行后可以觀察到:當 Eden 區無法容納大對象時,會觸發 Minor GC,若 Survivor 區仍無法容納,則會通過分配擔保機制將對象直接晉升到老年代。
2. 大對象直接進入老年代
????????大對象(需要大量連續內存的對象,如長字符串、數組)會被直接分配到老年代,這是為了避免大對象在新生代頻繁復制導致的性能損耗。
????????不同垃圾回收器對 "大對象" 的判定標準不同:
- G1 收集器:根據
-XX:G1HeapRegionSize
設置的區域大小和-XX:G1MixedGCLiveThresholdPercent
閾值判定 - Parallel Scavenge 收集器:由虛擬機根據堆內存情況動態決定,無固定閾值
3. 長期存活的對象進入老年代
????????JVM 為每個對象維護一個 "年齡計數器",用于判斷對象是否應晉升到老年代:
- 對象在 Eden 區出生,經過第一次 Minor GC 后存活并被移至 Survivor 區,年齡設為 1
- 每在 Survivor 區熬過一次 Minor GC,年齡增加 1 歲
- 當年齡達到閾值(默認 15 歲,可通過
-XX:MaxTenuringThreshold
設置)時,晉升到老年代
動態年齡調整:
虛擬機并非嚴格按固定年齡閾值晉升對象。當 Survivor 區中相同年齡的對象總大小超過 Survivor 空間的 50%(可通過-XX:TargetSurvivorRatio
調整)時,所有年齡大于或等于該年齡的對象會直接晉升到老年代。
注意:不同收集器的默認閾值不同,CMS 收集器默認閾值為 6,Parallel 收集器默認 15。
二、內存回收的觸發機制
????????JVM 的垃圾回收(GC)按回收范圍可分為 Partial GC(部分回收)和 Full GC(整堆回收),其中 Partial GC 又可細分為:
- Young GC:僅回收新生代
- Old GC:僅回收老年代(僅 CMS 支持)
- Mixed GC:回收新生代 + 部分老年代(僅 G1 支持)
1. Young GC 觸發條件
????????當新生代的 Eden 區分配滿時,觸發 Young GC。此時會有部分存活對象晉升到老年代,因此 Young GC 后老年代占用量可能上升。
2. Full GC 觸發條件
????????Full GC 會回收整個堆空間,觸發成本較高,常見觸發場景包括:
- 老年代空間不足
- 方法區(元空間)內存不足
- 調用
System.gc()
(建議避免) - 空間分配擔保失敗
3. 空間分配擔保機制
????????為確保 Minor GC 的安全性,JVM 會在 Minor GC 前進行空間分配擔保檢查:
- JDK 6 Update 24 前:檢查老年代最大可用連續空間是否大于新生代對象總大小或歷次晉升平均大小,否則觸發 Full GC
- JDK 6 Update 24 后:只要老年代連續空間大于新生代總大小或歷次晉升平均大小,就進行 Minor GC,否則觸發 Full GC
三、對象存活的判斷方法
????????垃圾回收的前提是準確判斷對象是否存活,JVM 主要采用兩種判斷算法:
1. 引用計數法
- 原理:為每個對象設置引用計數器,引用增加時 + 1,引用失效時 - 1,計數器為 0 則標記為可回收
- 缺陷:無法解決對象循環引用問題,因此主流 JVM 未采用
2. 可達性分析算法
- 原理:以 "GC Roots" 為起點,遍歷引用鏈,不可達的對象被標記為可回收
- GC Roots 包括:
- 虛擬機棧中引用的對象
- 本地方法棧中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 同步鎖持有的對象
對象死亡的兩次標記:
- 第一次標記:可達性分析后不可達的對象
- 第二次標記:檢查對象是否需要執行
finalize()
方法,若無需執行或已執行,則判定為死亡
注意:
finalize()
方法已被 JDK 9 標記為過時,建議避免使用。
四、引用類型與回收策略
????????JDK 1.2 后將引用分為四類,強度依次減弱,影響對象的回收時機:
引用類型 | 特點 | 應用場景 | |
---|---|---|---|
強引用 | Object obj = new Object() | 普通對象引用,內存不足時不會回收 | 一般對象引用 |
軟引用 | SoftReference | 內存不足時會被回收 | 內存敏感的緩存 |
弱引用 | WeakReference | 無論內存是否充足,GC 時都會回收 | 臨時緩存 |
虛引用 | PhantomReference | 不影響對象生命周期,僅用于跟蹤回收 | 管理直接內存 |
軟引用和弱引用可配合ReferenceQueue
使用,當引用對象被回收時,引用實例會被加入隊列,便于后續處理。
五、實戰建議與最佳實踐
- 避免創建大對象:大對象直接進入老年代,可能頻繁觸發 Full GC
- 合理設置新生代大小:新生代過小會導致 Young GC 頻繁,過大則會延長 GC 時間
- 選擇合適的垃圾收集器:
- 追求吞吐量:選擇 Parallel Scavenge + Parallel Old
- 追求低延遲:選擇 G1 或 ZGC
- 監控 GC 性能:通過
-XX:+PrintGCDetails
、-XX:+PrintGCLogs
等參數分析 GC 日志 - 慎用
System.gc()
:該方法僅為建議,可能觸發 Full GC 影響性能
六、總結
????????JVM 的內存分配與回收機制是自動內存管理的核心,其設計遵循 "分代收集" 思想,通過不同區域的針對性策略實現高效的內存管理。理解這些原則有助于開發者寫出更優的代碼,避免常見的內存問題(如 OOM),并在必要時進行有效的性能調優。
????????掌握內存分配規律、GC 觸發機制和引用類型特性,是深入理解 JVM 的重要一步,也是應對高并發、高性能場景的必備知識。