基於《深入理解 Java 虛擬機》(第 3 版) 核心知識體系,整理 JVM 運行時數據區、垃圾收集算法與收集器、類加載機制、對象模型等核心內容,並提煉生產環境調優實戰經驗。
一、JVM 運行時數據區 (JDK 8+ 模型)
| 區域 | 線程共享性 | 核心功能 | 關鍵參數 |
| 堆 | 共享 | 對象實例、數組分配主戰場,GC 管理核心區域 | -Xms, -Xmx, -Xmn |
| 方法區 | 共享 | 類元數據、常量、靜態變量、JIT 編譯代碼 | -XX:MetaspaceSize, -XX:MaxMetaspaceSize |
| 虛擬機棧 | 隔離 | 方法執行內存模型:棧幀(局部變量表、操作數棧、動態鏈接、返回地址) | -Xss |
| 本地方法棧 | 隔離 | Native 方法調用服務 | -XX:MaxDirectMemorySize |
| 程序計數器 | 隔離 | 當前線程執行字節碼行號指示器 | 無配置 |
關鍵變化:JDK 8 移除永久代,類元數據移至元空間(直接內存);JDK 11+ 字符串常量池、靜態變量已完全在堆中。
二、堆內存分代佈局與對象分配流程
堆
├── 新生代
│ ├── Eden 區 (80%)
│ ├── From Survivor (10%)
│ └── To Survivor (10%)
└── 老年代
- 默認比例:Eden : S0 : S1 = 8 : 1 : 1 (
-XX:SurvivorRatio=8)
- 分配規則:優先 Eden → 大對象直接進老年代(
PretenureSizeThreshold) → 長期存活對象進老年代(MaxTenuringThreshold, 默認 15) → 動態年齡判斷
- TLAB:線程本地分配緩衝,避免多線程分配競爭,
-XX:+UseTLAB 默認開啟
三、對象模型:創建、佈局與訪問定位
3.1 對象創建流程
- 類加載檢查:常量池定位符號引用 → 類加載/解析/初始化
- 內存分配:指針碰撞(Serial/ParNew) 或 空閒列表(CMS/G1) + TLAB
- 初始化零值:除對象頭外字段置零
- 設置對象頭:HashCode、GC 年齡、鎖狀態、類元數據指針、數組長度
- 執行
<init> 方法:按代碼順序賦值、調用構造器
3.2 對象內存佈局
[對象頭 Header] [實例數據 Instance Data] [對齊填充 Padding]
- Mark Word (8/16 bytes): HashCode、GC分代年齡、鎖標記、線程ID、時間戳
- Klass Pointer: 指向方法區類元數據
- Array Length (數組特有): 數組長度
3.3 對象訪問定位
- 句柄訪問:reference → 句柄池 → 實例數據/類型數據;優點:GC 移動對象只改句柄池,reference 穩定
- 直接指針訪問:reference → 對象地址 → 對象頭類型指針 → 類元數據;優點:少一次間接訪問,HotSpot 默認
四、垃圾判定算法
| 算法 | 原理 | 優點 | 缺點 | 應用 |
| 引用計數 | 對象引用計數器 +1/-1,為0可回收 | 實時性高、實現簡單 | 無法解決循環引用 | Python、COM、瀏覽器 JS 早期 |
| 可達性分析 | GC Roots 為起點,搜索引用鏈,不可達即垃圾 | 解決循環引用、準確 | 需 STW 掃描 Roots | Java、Go、V8、.NET CLR |
GC Roots:虛擬機棧引用對象、本地方法棧引用對象、方法區類靜態屬性/常量引用對象、同步鎖持有對象、JNI 全局引用等。
五、四種引用類型與生命週期
| 引用類型 | 強度 | 回收時機 | 典型用途 | API |
| 強引用 | 最強 | 永不回收(OOM 也不回收) | 普遍對象引用 | Object obj = new Object() |
| 軟引用 | 次強 | 內存不足前回收 | 緩存、圖片加載 | SoftReference |
| 弱引用 | 較弱 | 下次 GC 回收 | 監聽器、ThreadLocalMap | WeakReference |
| 虛引用 | 最弱 | 回收時收到通知 | DirectBuffer 內存釋放、堆外內存管理 | PhantomReference |
六、三大經典 GC 算法
| 算法 | 核心思想 | 優點 | 缺點 | 適用場景 |
| 標記-清除 | 標記垃圾 → 統一清除 | 實現簡單 | 效率低、產生碎片 | 老年代 (CMS 初期) |
| 標記-復制 | 劃分兩塊,存活復制到另一塊 | 無碎片、分配快 | 內存利用率 50% | 新生代 (Serial/ParNew/Parallel Scavenge) |
| 標記-整理 | 標記 → 存活向一端移動 → 清理邊界外 | 無碎片、利用率高 | 移動對象需更新引用、效率較低 | 老年代 (Serial Old/Parallel Old/G1) |
七、主流垃圾收集器全景圖
| 收集器 | 代 | 算法 | 線程 | 特點 | 適用場景 |
| Serial | 新/老 | 復制/整理 | 單 | 簡單高效、無線程交互開銷 | Client 模式、單核、小堆 |
| ParNew | 新 | 復制 | 多 | Serial 多線程版,配合 CMS | Server 模式、多核、需低停頓 |
| Parallel Scavenge | 新 | 復制 | 多 | 關注吞吐量、自適應調節 | 後台計算、批處理、無交互 |
| Serial Old | 老 | 整理 | 單 | Serial 老年代版 | Client 模式、CMS 降級備選 |
| Parallel Old | 老 | 整理 | 多 | Parallel Scavenge 老年代版 | 吞吐量優先場景 |
| CMS | 老 | 標記-清除 | 多 | 並發標記/清除、低停頓、碎片 | 交互應用、Web 服務、堆<4GB |
| G1 | 全區域 | 標記-整理(局部復制) | 多 | 分區、可預測停頓、面向大堆 | Server 模式、堆>4GB、JDK 9+ 默認 |
| ZGC | 全區域 | 染色指針+讀屏障 | 多 | 亞毫秒停頓、TB 級堆 | 超大堆、極低延遲敏感 |
| Shenandoah | 全區域 | 連接矩陣+寫屏障 | 多 | 亞毫秒停頓、與 ZGC 競品 | 超大堆、OpenJDK 主推 |
CMS 關鍵流程
- 初始標記 (STW) - 標記 GC Roots 直接可達
- 並發標記 - 遍歷對象圖,與用戶線程並發
- 重新標記 (STW) - 修正並發階段變動,多線程並行
- 並發清除 - 清理垃圾,與用戶線程並發
缺點:CPU 敏感、浮動垃圾、碎片(Full GC 才整理)。
G1 核心創新
- Region 分區:堆劃分為 2048 個大小相同 Region(1-32MB),邏輯上屬於新生代/老年代
- 收集價值優先:後台維護優先列表,優先回收垃圾密度高的 Region
- 混合收集:Young GC + 部分老年代 Region 同時回收
- SATB:Snapshot-At-The-Beginning,並發標記用寫屏障記錄舊引用,保證不漏標
八、GC 觸發條件與調優參數
| GC 類型 | 觸發條件 | 關鍵參數 |
| Minor GC / Young GC | Eden 區分配失敗 | -XX:NewRatio, -XX:SurvivorRatio, -XX:MaxTenuringThreshold |
| Full GC | 老年代空間不足、Metaspace 不足、System.gc()、堆內存整理 | -XX:+DisableExplicitGC, -XX:MetaspaceSize |
| Mixed GC (G1) | 老年代佔用達到閾值(InitiatingHeapOccupancyPercent, 默認 45%) | -XX:InitiatingHeapOccupancyPercent, -XX:G1MixedGCLiveThresholdPercent |
G1 核心調優參數
-XX:+UseG1GC # 啟用 G1
-Xmx32g # 最大堆 32G
-XX:MaxGCPauseMillis=200 # 目標最大停頓 200ms (核心調優手柄)
-XX:G1HeapRegionSize=16m # Region 大小 16MB (2的冪, 1-32MB)
-XX:ParallelGCThreads=8 # STW 工作線程數
-XX:ConcGCThreads=2 # 並發標記線程數
-XX:InitiatingHeapOccupancyPercent=45 # 啟動標記周期閾值
避免設置:-Xmn、-XX:NewRatio 等固定新生代大小參數,會干擾 G1 自適應調節。
G1 Full GC 觸發場景與應對
| 場景 | 原因 | 應對策略 |
| 併發模式失敗 | 標記周期未完成老年代已滿 | 增大堆、提前啟動標記(-XX:InitiatingHeapOccupancyPercent)、增加並發線程 |
| 晉升/疏散失敗 | Survivor/老年代無空間容納存活對象 | 增大 -XX:G1ReservePercent、提前啟動標記、增加並發線程 |
| 巨型對象分配失敗 | 找不到連續 Region 存放 >50% Region 的對象 | 避免巨型對象、增大 Region Size、增加堆 |
九、類加載機制:雙親委派與生命週期
9.1 五大階段
- 加載:全限定名獲取字節流 → 方法區存儲結構 → 生成 Class 對象
- 驗證:文件格式/元數據/字節碼/符號引用 四重校驗
- 準備:靜態變量分配內存、賦零值
- 解析:符號引用 → 直接引用(類/接口/字段/方法/方法類型/方法句柄/調用點)
- 初始化:執行
<clinit>() (靜態變量賦值+靜態代碼塊),父類優先
9.2 主動引用觸發初始化 (5 種)
new、getstatic、putstatic、invokestatic 字節碼指令
- 反射調用
java.lang.reflect
- 初始化子類觸發父類初始化
- JVM 啟動主類
- MethodHandle 解析為靜態字段/方法句柄
9.3 雙親委派模型
啟動類加載器 → 擴展類加載器 → 應用程序類加載器 → 自定義類加載器
↑ ↑ ↑ ↑
父類加載器 父類加載器 父類加載器 子類加載器
- 委派機制:收到請求先委派給父類,父類無法加載時子類嘗試
- 優勢:核心類庫統一由啟動類加載器加載,防止偽造核心類、保證類唯一性
- 破壞場景:JNDI/SPI/JDBC 需要父類加載器加載用戶代碼(線程上下文類加載器)