JVM 內存結構與垃圾回收核心機制
基於《深入理解 Java 虛擬機》第 2-3 章核心知識體系,系統梳理 JVM 運行時數據區、對象內存佈局、訪問定位、垃圾判定算法、回收算法、收集器特性與內存分配策略。
JVM 運行時數據區 (JDK 1.8+)
| 區域 | 線程私有/共享 | 核心功能 | OOM 風險 |
| 程序計數器 | 私有 | 字節碼行號指示器,唯一不會 OOM 區域 | 無 |
| 虛擬機棧 | 私有 | 棧幀 (局部變量表、操作數棧、動態鏈接、返回地址) | StackOverflowError / OOM |
| 本地方法棧 | 私有 | Native 方法服務 | StackOverflowError / OOM |
| 堆 | 共享 | 對象實例分配區,GC 主戰場,分代 (新生代/老年代) | 高 (java.lang.OutOfMemoryError: Java heap space) |
| 方法區 | 共享 | 類元數據、常量、靜態變量、JIT 代碼緩存 (JDK8+ Metaspace,本地內存) | Metaspace OOM |
| 直接內存 | 共享 | NIO DirectBuffer、Unsafe 分配,不受堆限制,受本地內存限制 | Direct buffer memory |
對象內存佈局
┌─────────────────────────────────────┐
│ Object Header (對象頭) │ ← 12/16 bytes (32/64位)
│ - Mark Word (哈希碼、GC年齡、鎖狀態) │
│ - Class Pointer (類元數據指針) │
│ - Array Length (數組才有) │
├─────────────────────────────────────┤
│ Instance Data (實例數據) │ ← 字段數據,含父類字段
│ - 按分配策略排序 (longs/doubles → ints → shorts/bytes → references) │
├─────────────────────────────────────┤
│ Padding (對齊填充) │ ← 8 字節對齊
└─────────────────────────────────────┘
對象訪問定位方式
| 方式 | 原理 | 優點 | HotSpot 採用 |
| 句柄訪問 | reference → 句柄池 → (實例池指針, 類數據指針) | reference 穩定,移動對象只改句柄 | 否 |
| 直接指針 | reference → 直接指向對象,對象頭含類指針 | 少一次間接訪問,速度快 | 是 |
垃圾判定算法
1. 可達性分析 (主流)
GC Roots 包含:
- 虛擬機棧引用的對象 (局部變量表)
- 方法區類靜態屬性引用的對象
- 方法區常量引用的對象
- 本地方法棧 JNI 引用的對象
2. 四種引用強度 (強→弱)
| 引用類型 | API | 回收時機 | 典型場景 |
| 強引用 | Object obj = new Object() | 永不回收 (寧願 OOM) | 核心業務對象 |
| 軟引用 | SoftReference<T> | 內存不足前回收 | 緩存 (圖片、網絡數據) |
| 弱引用 | WeakReference<T> | 下次 GC 必回收 | 監聽器、ThreadLocalMap、緩存 |
| 虛引用 | PhantomReference<T> | 回收時收到通知 | 堆外內存釋放、精確回收追蹤 |
垃圾回收算法演進
| 算法 | 原理 | 優點 | 缺點 | 適用區域 |
| 標記-清除 | 標記存活 → 清除死亡 | 無需移動對象 | 碎片化、效率低 | 老年代 (CMS) |
| 標記-整理 | 標記存活 → 壓縮到一端 | 無碎片 | 需移動對象、更新引用 | 老年代 (Parallel Old, G1) |
| 復制 | 劃分兩塊,存活復制到另一塊 | 無碎片、分配快 (指針碰撞) | 空間利用率 50% | 新生代 (Eden → Survivor) |
| 分代收集 | 新生代復制 + 老年代標記整理 | 針對對象存活特性優化 | 實現複雜 | 全堆 (所有商用 JVM) |
HotSpot 垃圾收集器全景
| 收集器 | 模式 | 算法 | 目標區域 | 特點 | JDK 版本狀態 |
| Serial | 單線程 | 復制/標記整理 | 新/老 | 簡單、客戶端模式默認 | 維護中 |
| ParNew | 多線程 | 復制 | 新生代 | Serial 多線程版,配合 CMS | Deprecated (JDK9), Removed (JDK14) |
| Parallel Scavenge | 多線程 | 復制 | 新生代 | 吞吐量優先、自適應調節 | JDK8 默認新生代 |
| Parallel Old | 多線程 | 標記整理 | 老年代 | Parallel Scavenge 老年代版 | JDK8 默認老年代 |
| CMS | 並發 | 標記-清除 | 老年代 | 低停頓、碎片化、CPU 敏感 | Deprecated (JDK9), Removed (JDK14) |
| G1 | 並發+分區 | 標記整理+復制 | 全堆 | 可預測停頓、Region 分區、JDK9+ 默認 | 長期主流 |
| ZGC | 並發 | 著色指針 | 全堆 | 亞毫秒停頓、TB 級堆、JDK11+ 實驗 | 生產就緒 (JDK15+) |
| Shenandoah | 並發 | 連接矩陣 | 全堆 | 超低停頓、RedHat 主導、JDK12+ | 生產就緒 |
核心內存分配規則
- 優先 Eden:新對象多在 Eden 分配,Eden 滿觸發 Minor GC
- 大對象直入老年代:
-XX:PretenureSizeThreshold 閾值 (如大數組、長字符串),避免 Survivor 來回復制
- 長期存活入老年代:年齡計數器達
-XX:MaxTenuringThreshold (默認 15,CMS 6) 晉升
- 動態年齡判定:Survivor 同齡對象總大小 > Survivor 一半,直接晉升
- 空間分配擔保:Minor GC 前檢查老年代剩餘空間 > 新生代所有對象/歷次晉升平均值,否則觸發 Full GC
GC 調優核心指標
- 吞吐量 = 用戶代碼時間 / (用戶代碼 + GC 時間) →
-XX:+UseParallelGC
- 停頓時間 = 垃圾收集線程佔用應用線程時間 →
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
- 堆大小:
-Xms -Xmx 設為相同值避免震蕩,通常設物理內存 60-80%
- 新生代比例:
-XX:NewRatio=2 (老年代:新生代=2:1)