CodeGym /課程 /JAVA 25 SELF /JVM 記憶體結構:堆疊(Stack)、堆(Heap)、PermGen/MetaSpace

JVM 記憶體結構:堆疊(Stack)、堆(Heap)、PermGen/MetaSpace

JAVA 25 SELF
等級 64, 課堂 0
開放

1. Java 行程的記憶體總覽

當你啟動一個 Java 程式時,JVM(Java Virtual Machine)會向作業系統索取一塊記憶體。有時很小,有時很大(尤其是你跑一個裝了很多模組的 Minecraft 時)。這塊記憶體被切成幾個關鍵區域,每一區都有自己的角色:

  • 堆疊(Stack) — 用於區域變數與方法呼叫。
  • 堆(Heap) — 用於所有透過 new 建立的物件。
  • 服務區域(PermGen/MetaSpace)— 用於類別的中繼資料、靜態欄位與其他「魔法」內容。

大致如下:

┌───────────────────────────────┐
│ JVM 行程                      │
│ ┌─────────────┐               │
│ │  Stack      │ ← 每個執行緒都有自己的堆疊!
│ └─────────────┘               │
│ ┌─────────────┐               │
│ │  Heap       │ ← 所有執行緒共享
│ └─────────────┘               │
│ ┌───────────────┐             │
│ │ PermGen/      │ ← 類別中繼資料
│ │ MetaSpace     │
│ └───────────────┘             │
└───────────────────────────────┘

為什麼這很重要?

  • 理解記憶體結構有助於寫出更高效且安全的程式碼。
  • 更容易診斷 StackOverflowError 或 OutOfMemoryError 之類的錯誤。
  • 不再害怕「垃圾回收」與「記憶體洩漏」——你會知道該到哪裡找問題。

2. 堆疊(Stack):快速、區域,但不是永久

堆疊是為每個執行緒分別配置的特殊記憶體區域。堆疊就像一疊盤子:最後放上去的,要等上面所有的都拿走之後才能先拿。也就是說,堆疊遵循 LIFO(Last In, First Out,後進先出)原則。

堆疊有什麼用?

在堆疊中存放:

  • 方法的區域變數(例如方法內的 int x = 5;)。
  • 返回位址(方法呼叫結束後要返回到哪裡)。

每次你呼叫一個方法,堆疊中會加入新的框架(stack frame)——類似一個盒子,裡頭放著該方法的所有區域變數與一些輔助資訊。當方法執行完畢,它的框架會被移除——裡面的區域變數也隨之消失。

範例

public static void main(String[] args) {
    int a = 10;           // a 位於 main 的堆疊
    int b = sum(a, 5);    // 呼叫 sum
}

public static int sum(int x, int y) {
    int result = x + y;   // x、y、result 位於 sum 的堆疊
    return result;
}
  • 當呼叫 sum 時,會在堆疊中為它建立獨立的框架(frame)。
  • sum 結束後,它的變數會消失。

變數的生命週期

區域變數只在其所屬的方法執行期間存在。一旦方法結束——它們就不再存在,記憶體會立刻被釋放。

堆疊溢位

如果你不小心(或故意)寫了無窮遞迴,每次方法呼叫都會在堆疊中加入新的框架。某個時刻堆疊空間會用盡,你會得到:

Exception in thread "main" java.lang.StackOverflowError

範例:

public static void main(String[] args) {
    recurse();
}
public static void recurse() {
    recurse(); // 無窮遞迴!
}

堆疊大小

堆疊的大小是有限的——通常每個執行緒只有幾個 MB(可用參數 -Xss 設定)。如果堆疊用盡——程式會因錯誤而終止。

3. 堆(Heap):放置你物件的地方

堆(Heap)是所有執行緒共享的記憶體區域,透過 new 建立的所有物件與陣列都住在這裡。物件導向的各種魔法基本上都發生在堆中。

物件如何進入堆?

String s = new String("Hello");
int[] arr = new int[10];
  • 變數 s 是參考(reference),它位於堆疊。
  • String 物件本身與陣列 arr 位於堆。

物件的生命週期

只要仍存在至少一個強參考(strong reference)指向某個物件,該物件就會活在堆中。一旦沒有人再指向它——它就成為「垃圾」,可以被垃圾回收器(GC)移除。

記憶體管理

不同於 C/C++ 需要你自行釋放記憶體(free、delete),在 Java 中由 GC 負責。你不能顯式釋放物件,但可以把所有指向它的參考設為 null——之後它就成為回收候選。

示意:哪裡放了什麼?

Stack (main)
  └─ s ─┬────────────┐
         │           │
         ▼           │
      Heap           │
   ┌─────────────┐   │
   │ String "Hello"◄──┘
   └─────────────┘

堆的特性

  • 整個 JVM 行程只有一個堆。
  • 可在啟動時設定堆的大小(-Xmx、-Xms)。
  • 如果堆中沒有可用空間,且 GC 無法釋放記憶體——程式會拋出 OutOfMemoryError。

4. PermGen 與 MetaSpace:類別存在哪裡?

當你寫下 class MyClass { ... } 並執行程式時,JVM 需要有地方存放與該類別相關的一切——方法、欄位、位元組碼、靜態變數、常數,甚至字串常值。為此,JVM 有一個專門存放「類別」的記憶體區域。

在 Java 8 之前,這個區域稱為 PermGen(Permanent Generation)。但它有不少問題——例如大小是固定的,如果空間不足,應用程式就會直接以 OutOfMemoryError: PermGen space 錯誤終止。

自 Java 8 起出現了更彈性的區域——MetaSpace。它取代了舊的 PermGen,並且可以自動擴張,使用系統可用的實體記憶體(在可用範圍內)。

PermGen(Java 8 之前)

  • PermGen 存放類別中繼資料、靜態欄位、字串常值。
  • PermGen 大小受限(預設很小),可以用參數 -XX:MaxPermSize=256m 增加。
  • 如果應用程式動態載入大量類別(例如在 web 伺服器中),PermGen 可能「用完」,你會看到錯誤:
java.lang.OutOfMemoryError: PermGen space
  • 問題:當類別動態卸載時,PermGen 並不總是能正確清理(例如重新載入 web 應用時)。

MetaSpace(Java 8+)

  • 從 Java 8 開始 PermGen 消失,改為 MetaSpace。
  • MetaSpace 仍然存放類別的中繼資料,但現在位於原生記憶體(在 Java Heap 之外)。
  • MetaSpace 預設不設上限(僅受系統記憶體限制),也可用 -XX:MaxMetaspaceSize=512m 設定上限。
  • 記憶體不足時的錯誤現在長這樣:
java.lang.OutOfMemoryError: Metaspace
  • MetaSpace 中也會放入靜態欄位、方法、類別資訊。

示意:整體是如何運作的

┌───────────────────────────────┐
│ JVM 行程                      │
│ ┌─────────────┐               │
│ │  Stack      │ ← 區域變數、方法呼叫
│ └─────────────┘               │
│ ┌─────────────┐               │
│ │  Heap       │ ← 物件、陣列、所有透過 new 的東西
│ └─────────────┘               │
│ ┌───────────────┐             │
│ │ MetaSpace     │ ← 類別中繼資料、靜態欄位
│ └───────────────┘             │
└───────────────────────────────┘

為什麼這很重要?

如果你寫的是一般桌面或伺服器應用,多半不會遇到 PermGen 或 MetaSpace 的錯誤。但如果你使用動態載入類別(例如外掛、web 應用、或像 Spring 這種會載入/卸載大量類別的框架),那麼了解 MetaSpace 是必備知識!

5. 圖解:JVM 記憶體結構

flowchart TD
    subgraph JVM
        direction TB
        Stack1["Stack (Thread 1)"]
        Stack2["Stack (Thread 2)"]
        Heap[Heap]
        MetaSpace[MetaSpace]
    end
    Stack1 --引用--> Heap
    Stack2 --引用--> Heap
    Heap --使用類別自--> MetaSpace
  • 每個執行緒都有自己的堆疊。
  • 所有堆疊都可以參考堆中的物件。
  • 堆中的物件「知道」自己的類別,而類別資訊位於 MetaSpace 中。

6. 範例:在實際程式碼中長什麼樣

public class MemoryDemo {
    public static void main(String[] args) {
        int x = 42; // x 位於 main 的堆疊
        String s = "Hello!"; // s — 參考在堆疊,String 物件在堆,字串常值 "Hello!" 在 MetaSpace
        Person p = new Person("Alice"); // p — 參考在堆疊,Person 物件在堆

        // 呼叫方法以建立新的堆疊框架
        printPerson(p);
    }

    public static void printPerson(Person person) {
        // person — 位於 printPerson 堆疊中的參考
        System.out.println(person.getName());
    }
}

class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() { return name; }
}

解析:

  • x — 區域變數,存在於 main 的堆疊。
  • s — 參考在堆疊,String 物件在堆,而字串常值 "Hello!" 在 MetaSpace。
  • p — 參考在堆疊,Person 物件在堆。
  • Person 類別及其所有方法/欄位——在 MetaSpace(類別中繼資料)。
  • 呼叫 printPerson(p) 會建立新的堆疊框架;在其中,區域參考 person 指向同一個堆中的物件。

7. JVM 如何管理記憶體:簡短 FAQ

我能管理堆疊嗎?
不行,堆疊完全由 JVM 控制。你只能在啟動時設定其大小(-Xss)。

我能管理堆嗎?
部分可以:堆的大小可在啟動時設定(-Xmx、-Xms)。清理由垃圾回收器(GC)負責。

我能管理 MetaSpace 嗎?
可以設定上限(-XX:MaxMetaspaceSize),但通常不需要。

記憶體不足時會發生什麼?
— 如果堆疊用完 — StackOverflowError。
— 如果堆用完 — OutOfMemoryError: Java heap space。
— 如果 MetaSpace 用完 — OutOfMemoryError: Metaspace。

8. 使用記憶體時的常見錯誤

錯誤 1:因無窮遞迴導致 StackOverflowError。 最常見的原因是忘了設計遞迴的終止條件。例如方法不停地呼叫自己。JVM 無法把堆疊無限制擴張,程式終將「倒下」。

錯誤 2:因堆滿了導致 OutOfMemoryError。 如果你建立了太多仍被變數/集合引用的物件(例如不斷往清單加入元素但從不移除),堆可能會用完。

錯誤 3:OutOfMemoryError: PermGen space / Metaspace。 如果你使用外掛或動態載入大量類別,而 MetaSpace 未被正確清理(例如類別未正確卸載),MetaSpace 的空間可能用盡。

錯誤 4:混淆了「參考」與「物件」。 許多新手會搞混:堆疊中的 Person 型別變數只是參考,真正的物件在堆中。

錯誤 5:期待垃圾回收器能立刻刪掉所有東西。 GC 是「看心情」運作的(其實是依內部演算法與記憶體壓力),不是在你把參考設為 null 的瞬間就執行。不要指望立即釋放記憶體。

1
任務
JAVA 25 SELF, 等級 64, 課堂 0
上鎖
StackOverflowError – 淘氣的遞迴連鎖 🧙‍♂️
StackOverflowError – 淘氣的遞迴連鎖 🧙‍♂️
1
任務
JAVA 25 SELF, 等級 64, 課堂 0
上鎖
OutOfMemoryError – 貪婪的記憶吞噬者 💾
OutOfMemoryError – 貪婪的記憶吞噬者 💾
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION