首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java GC 機制與記憶體分配策略深度解析

     編輯:Java 相關知識

前言

在 Java 虛擬機(JVM)的自動記憶體管理機制下,開發者無需為每個 new 操作編寫配對的 delete/free 代碼。然而,當發生記憶體洩漏或溢出問題時,若不了解虛擬機如何使用記憶體,排查錯誤將變得異常困難。本文將系統梳理 Java 垃圾收集(GC)機制、對象存活判定算法、垃圾收集算法、常見收集器以及記憶體分配與回收策略,幫助開發者建立完整的 JVM 記憶體管理知識體系。

一、對象存活判定算法

GC 在回收堆內存前,首先要確定哪些對象「存活」,哪些已經「死去」。主要有兩種判定算法:

1. 引用計數算法

核心思想: 給對象添加一個引用計數器,每當有一個地方引用它時計數器加 1,引用失效時減 1。計數器為 0 的對象即為不可用對象。

  • 優點: 實現簡單,判斷效率高。
  • 缺點: 難以解決對象間相互循環引用的問題(如 A 引用 B,B 引用 A,計數器永不為 0)。

2. 可達性分析算法(主流算法)

核心思想: 通過一系列「GC Roots」對象作為起始點,向下搜索形成引用鏈。若對象到 GC Roots 無任何引用鏈相連,則判定為可回收對象。

可達性分析示意圖

可作為 GC Roots 的對象包括:

  1. 虛擬機棧中引用的對象(棧幀中的局部變量表)
  2. 方法區中類靜態屬性引用的對象
  3. 方法區中常量引用的對象
  4. 本地方法棧中 JNI 引用的對象

3. 生存還是死亡?—— finalize() 最後的機會

即使在可達性分析中不可達,對象也並非「非死不可」,需經歷兩次標記:

  1. 第一次標記: 發現無引用鏈後標記並篩選,判斷是否有必要執行 finalize() 方法(未覆寫或已被調用過視為「不必要」)。
  2. 進入 F-Queue: 若需執行 finalize(),對象被放入 F-Queue,由低優先級的 Finalizer 線程觸發執行(不保證等待結束)。
  3. 第二次標記: GC 對 F-Queue 中對象進行小規模標記。若對象在 finalize() 中重新與引用鏈建立關聯(如 this 賦值給類變量),則移出「即將回收」集合;否則真正被回收。

注意: finalize() 只會被系統自動調用一次,建議避免使用,改用 try-finally 或 try-with-resources 釋放資源。

二、垃圾收集算法

1. 標記-清除算法

  • 過程: 標記所有需回收對象 → 統一回收。
  • 不足: 效率較低;產生大量不連續記憶體碎片,可能導致大對象無法分配而提前觸發 GC。

2. 複制算法

  • 過程: 將可用記憶體劃分為大小相等的兩塊,每次只用一塊。用完時將存活對象複製到另一塊,再清理已用空間。
  • 優點: 實現簡單,運行高效,無碎片問題。
  • 代價: 記憶體縮小為原來一半。

3. 標記-整理算法

  • 過程: 標記需回收對象 → 讓所有存活對象向一端移動 → 清理端邊界外記憶體。
  • 適用: 老年代(對象存活率高、無額外空間擔保)。

4. 分代收集算法(主流商用 JVM 採用)

  • 核心思想: 根據對象存活周期劃分記憶體區域,針對不同區域特點選擇適當算法。
  • 新生代: 大批對象死去、少量存活 → 採用複制算法(Eden + 兩個 Survivor 區)。
  • 老年代: 對象存活率高、無額外空間 → 採用標記-清除或標記-整理算法。

三、HotSpot 實現的分代結構

  • 新生代: Eden 空間 + 兩個 Survivor 空間(From/To)。新對象優先在 Eden 分配,Minor GC 後存活對象在 Survivor 間輪換,年齡達閾值(默認 15)晉升老年代。
  • 老年代: 存放長期存活對象,空間較大,Major GC(Full GC)頻率低。

四、常見垃圾收集器

垃圾收集器組合關係圖

連線表示可搭配使用。虛擬機所在區域標識屬於新生代或老年代收集器。

核心概念:Stop-the-World (STW)

任何 GC 算法中都會發生 STW,意味著 JVM 為執行 GC 停止應用程序線程,僅 GC 線程工作。GC 優化核心往往是減少 STW 時間。

主要收集器特點:

收集器類型線程算法特點
Serial新生代單線程複制簡單高效,Client 模式默認,STW 時間長
ParNew新生代多線程複制Serial 多線程版,與 CMS 配合
Parallel Scavenge新生代多線程複制關注吞吐量,自適應調節
Serial Old老年代單線程標記-整理Serial 老年代版
Parallel Old老年代多線程標記-整理Parallel Scavenge 老年代版
CMS老年代多線程標記-清除低停頓,並發收集,CPU 敏感,有碎片
G1全區域多線程標記-整理+複制面向服務端,可預測停頓,Region 劃分

CMS 收集器四階段:

  1. 初始標記(STW,快)
  2. 並發標記(與用戶線程並發)
  3. 重新標記(STW,修正並發標記變動)
  4. 並發清除(與用戶線程並發)

G1 收集器四階段:

  1. 初始標記(STW,修改 TAMS)
  2. 並發標記(可達性分析)
  3. 最終標記(STW,合併 Remembered Set Logs)
  4. 篩選回收(按回收價值排序,制定計劃)

五、記憶體分配與回收策略

  1. 對象優先在 Eden 分配: Eden 不足觸發 Minor GC。
  2. 大對象直接進入老年代: 避免在 Eden/Survivor 間大量複製(長字符串、大數組)。
  3. 長期存活對象進入老年代: 年齡計數器達 MaxTenuringThreshold(默認 15)晉升。
  4. 動態對象年齡判定: Survivor 中相同年齡對象總大小超過 Survivor 一半,直接晉升老年代。
  5. 空間分配擔保: Minor GC 前檢查老年代最大連續空間是否大於新生代所有對象總空間,不足則視 HandlePromotionFailure 設置決定是否冒險。

六、總結

記憶體回收與垃圾收集器是影響系統性能、併發能力的關鍵因素。JVM 提供多種收集器和大量調節參數,旨在根據實際應用場景選擇最優方案。沒有固定的最佳收集器組合,沒有萬能的調優參數,唯有結合業務特性、監控數據、壓測結果,持續觀測與調整,才能榨乾硬件性能。

參考資料:《深入理解 Java 虛擬機》(周志明)

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved