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

JVM 記憶體區域與異常深度解析

     編輯:Java 相關知識

JVM 運行時資料區域完整圖解

Java 程式運行在 JVM 上,由 JVM 管理記憶體,這與 C++ 手動管理最大不同。雖有 JVM 管控,但開發者仍需理解其管理機制。本文基於 HotSpot 虛擬機,詳細描述 JVM 記憶體分佈、物件儲存訪問、各區域可能出現的異常。

JVM 運行時資料區域劃分

依據 JVM 規範,管理的記憶體分為以下幾個運行時資料區域:

1. 虛擬機棧

  • 特性:線程私有,隨線程創建/銷毀。
  • 功能:描述 Java 方法執行記憶體模型。每個方法執行會創建一個棧幀,存儲區域變數表、操作數棧、動態連結、方法出口等。
  • 區域變數表:存放編譯期可知的基本類型、引用類型、返回地址。long/double 佔 2 個 Slot,其餘佔 1 個。大小在編譯期確定,運行期不變。

異常:

  • StackOverflowError:線程請求棧深度超過最大允許深度。
  • OutOfMemoryError:虛擬機棧可動態擴展時,無法申請足夠記憶體。

2. 本地方法棧

  • 同樣線程私有,為 Native 方法服務。
  • HotSpot 將虛擬機棧與本地方法棧合二為一實作。
  • 同樣拋出 StackOverflowError 和 OutOfMemoryError。

3. 程式計數器

  • 線程私有,可視為字節碼行號指示器。
  • 執行 Java 方法時記錄正在執行的字節碼地址;執行 Native 方法時值為 undefined。
  • 佔用空間極小,唯一不會拋出 OutOfMemoryError 的區域。

4. Java 堆

  • 線程共享,JVM 啟動時創建,最大記憶體區塊。
  • 主要儲存物件實例(含陣列),GC 主要工作區域。
  • 可處於不連續物理記憶體,邏輯連續即可。大小可固定或可擴展。
  • 異常:無法分配實例且無法擴展時拋出 OutOfMemoryError。

5. 方法區

  • 線程共享,儲存已加載類別資訊、常數、靜態變數、即時編譯器編譯後的代碼。
  • 不需連續記憶體,大小可固定/擴展,可選擇不實作 GC。
  • 異常:無法滿足記憶體分配需求時拋出 OutOfMemoryError。

6. 直接記憶體

  • 非 JVM 管理區域,但常被使用(如 NIO)。
  • 總大小受限於物理記憶體,不足時同樣拋出 OutOfMemoryError。

物件在記憶體中的儲存與訪問

1. 物件創建流程

  1. 遇到 new 指令,從常數池定位類符號引用,檢查類是否已加載初始化,否則執行類加載(失敗拋 ClassNotFoundException)。
  2. 類加載檢查通過後,為物件分配堆記憶體。大小由類決定,加載後固定。
  3. 分配方式視記憶體是否連續:
    • 指標碰撞:連續記憶體,指標劃分已用/未用區。
    • 空閒列表:不連續,維護列表記錄可用塊。
  4. 解決併發分配問題:
    • CAS + 失敗重試,保證原子性。
    • TLAB (Thread Local Allocation Buffer):每線程預分配一塊專用區,減少同步。
  5. 分配完成後,除物件頭外,記憶體初始化為零值;執行 init 方法完成初始化。

2. 物件記憶體佈局

HotSpot 中物件分為三部分:

  • 物件頭:運行時資料 + 類型指標。運行時資料含哈希碼、GC 分代年齡、鎖狀態等,32/64 位分別佔 32/64 位。類型指標指向類元資料(非必須)。
  • 實例資料:程式碼定義的各類型字段內容。分配策略:long/double → int → short/char → byte/boolean → oop。相同類型集中分配,父類字段優於子類。
  • 對齊填充:非必須,HotSpot 以 8 位元組為單位管理,物件大小非 8 倍數時補齊。

3. 物件訪問定位

  • 句柄訪問:堆中劃分句柄池,引用存句柄地址,句柄存實例與類型具體地址。物件移動時只需修改句柄。
  • 直接指標訪問:引用直接存實例堆地址,物件頭必須包含類型指標。優點:減少一次定位,大量訪問效能優勢明顯。HotSpot 採用此方式。

運行時記憶體異常實戰演示

主要異常:OutOfMemoryError、StackOverflowError。除程式計數器外,所有區域皆可能發生。

1. Java 堆溢出

-Xms20m -Xmx20m -XX:HeapDumpOnOutOfMemoryError

public class HeapOOM {
    public static void main(String[] args) {
        List<HeapOOM> list = new ArrayList<>();
        while (true) {
            list.add(new HeapOOM()); // 持有引用,阻止 GC
        }
    }
}

2. 虛擬機棧/本地方法棧溢出

-Xss128k

// 情況一:無限遞歸增加棧深度
public class StackOverflow {
    private int depth = 1;
    public void recursive() { depth++; recursive(); }
}

// 情況二:大量區域變數增加棧幀大小
public class StackOOM {
    private int depth = 1;
    public void recursive() {
        double i1, i2, i3...; // 大量變數
        depth++; recursive();
    }
}

測試證實:無論棧幀過大或容量過小,記憶體無法分配時均拋出 StackOverflowError。

3. 方法區/運行時常數池溢出

String.intern() 行為差異:

  • JDK 1.6:首次遇到字串複製到永久代,返回永久代引用。StringBuilder 建立的字串在堆中,故 intern() == str 為 false。
  • JDK 1.7+:常數池僅記錄首次出現實例引用,intern() 返回堆中同一實例,結果為 true。

因此 JDK 1.7+ 下常數池溢出測試實際觸發堆溢出。測試方法區溢出可用 CGLib 動態生成大量類:

while (true) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(OOMClass.class);
    enhancer.setUseCache(false);
    enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
    enhancer.create();
}

4. 直接記憶體溢出

-XX:MaxDirectMemorySize=30M

public class DirectMemoryOOM {
    public static void main(String[] args) {
        List<Buffer> buffers = new ArrayList<>();
        int i = 0;
        while (true) {
            System.out.println(++i);
            buffers.add(ByteBuffer.allocateDirect(1024 * 1024)); // 每次 1MB
        }
    }
}
// 第 31 次拋出: java.lang.OutOfMemoryError: Direct buffer memory

總結

本文系統闡述 JVM 記憶體結構、物件儲存訪問機制、各區域異常觸發條件與實戰代碼。參考《深入理解 Java 虛擬機(第二版)》,歡迎指正補充。

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