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

深入理解 JVM 內存分配:三大疑難問題實測解析

     編輯:Java 綜合教學

深入理解 JVM 內存分配:三大疑難問題實測解析

基於《深入理解 Java 虛擬機》學習筆記,作者在 Mac OS X + JDK 1.8.0_92 環境下(-XX:+UseSerialGC 串行收集器)設計三個實驗,探究 Eden 區初始佔用、Minor GC 後殘留內存、大對象直接進老年代等核心機制。本文結合 GC 日誌深度解析,還原 JVM 內存分配真相。

實驗環境統一參數

-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
-Xms20M -Xmx20M -Xmn10M  // 固定堆 20M,新生代 10M

問題一:空類啟動時 Eden 區為何已佔用 14%(1164K)?

現象

Heap
 def new generation total 9216K, used 1164K
 eden space 8192K, 14% used

解析

  • JVM 內部對象:類元數據、系統類、JNI 全局引用、線程結構、GC 相關數據結構等均在啟動階段分配於新生代。
  • TLAB(Thread Local Allocation Buffer):每個線程在 Eden 預分配一塊私有緩衝區,即使不創建業務對象,主線程啟動時也會佔用一部分 TLAB 空間。
  • 字符串常量、系統類加載器 等運行時數據結構初始化也會消耗新生代空間。

結論:Eden 區非零佔用是 JVM 運行時基礎設施初始化的必然結果,非業務對象佔用。

問題二:Minor GC 後 Eden 仍佔用 1354K,堆內存減少 579K 是誰被回收?

實驗代碼

byte[] a1..a6 = new byte[1MB]; // 分配 6MB 對象
-XX:SurvivorRatio=3 -XX:TenuringThreshold=1

GC 日誌關鍵行

[GC (Allocation Failure) DefNew: 6029K->1354K(8192K), 0.0033 secs]
6029K->5450K(18432K)

深度解析

  1. 對象分配與晉升:
    • a1 (1MB) 進入 Eden,觸發 GC 時存活,年齡達 1 被複製到 Survivor (S1)。
    • a2~a4 (3MB) 同理,但 Survivor 只有 2MB (10M * 1/(3+2) = 2MB),放不下,觸發動態年齡判定或直接過早晉升進老年代。
    • a5、a6 在 GC 後重新分配於 Eden(解釋 Eden 仍有 1354K 佔用)。
  2. 堆內存減少 579K (6029-5450):
    • 這部分是 GC 過程中臨時對象(如方法參數、中間計算結果、引用鏈遍歷產生的臨時對象)被回收。
    • 業務對象 a1~a6 均可達,未被回收,只是在新生代與老年代之間移動。

結論:Eden 非零是 GC 後新分配的對象;堆減少的是 GC 內部臨時對象,非業務數據。

問題三:大對象分配觸發「擔保失敗」直接進老年代?

實驗代碼

byte[] a1=2MB, a2=2MB, a3=2MB, a4=4MB; // 總 10MB,新生代 10MB
-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15

GC 日誌

[GC (Allocation Failure) [DefNew (promotion failed): 7144K->7144K(9216K)]
[Tenured: 4096K->4096K(6144K)] 7144K->6473K(15360K)]

機制還原

  1. a1、a2、a3 (6MB) 分配於 Eden,剩餘 Eden 空間不足 4MB。
  2. 分配 a4 (4MB) 觸發 Minor GC,嘗試將存活對象 (a1~a3) 複製到 Survivor 或老年代。
  3. Survivor 只有 1MB (10M/10),放不下;老年代剩餘 2MB (6M-4M),也放不下 6MB 存活對象 → 擔保失敗。
  4. JDK 6 Update 24 後引入 HandlePromotionFailure:擔保失敗時,不再 Full GC,直接在老年代分配大對象(a4 4MB 直接進老年代)。
  5. 老年代 4096K -> 4096K 未變,說明 a1~a3 仍在新生代(未移動),a4 繞過新生代直接進老年代,但日誌顯示老年代 used 未增,實際是 日誌聚合顯示限制,a4 確實在老年代。

結論:非自動設置大對象閾值,而是 擔保失敗觸發的逃生機制。大對象直接進老年代由 -XX:PretenureSizeThreshold 顯式控制,默認 0(不生效)。本例是分配失敗後的應急策略。

核心知識點總結

現象根本原因關鍵參數
啟動即佔用 EdenJVM 運行時基礎設施 + TLAB-XX:+PrintTLAB
Minor GC 後 Eden 非零GC 後續分配新對象-XX:+PrintGCDetails
堆減少但業務對象未回收回收 GC 臨時對象-XX:+PrintGCApplicationStoppedTime
大對象直接進老年代擔保失敗逃生機制-XX:HandlePromotionFailure

調試建議

  1. 加上 -XX:+PrintTenuringDistribution 觀察對象年齡分佈與動態閾值。
  2. 使用 jstat -gcutil pid 1000 或 VisualVM 動態監控各代佔用。
  3. 結合 -XX:+UnlockDiagnosticVMOptions -XX:+PrintGCApplicationConcurrentTime 分析應用線程與 GC 交互。

JVM 內存分配看似複雜,實則遵循「優先 Eden、大對象直入老年代、長期存活進老年代、空間擔保失敗觸發 Full GC」幾條鐵律。通過參數控制與日誌分析,所有行為皆可預測與調優。

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