Java 記憶體管理與 JVM 完全指南

深入理解 Java 虛擬機的記憶體架構、垃圾回收機制與效能調校


目錄


什麼是 JVM?

JVM(Java Virtual Machine,Java 虛擬機) 是執行 Java 程式的執行環境。

JVM 的作用

  • 跨平台:一次編譯,到處執行(Write Once, Run Anywhere)
  • 記憶體管理:自動垃圾回收(Garbage Collection, GC)
  • 執行 Bytecode:將 .class 檔案轉成機器碼執行

Java 程式執行流程

.java 原始碼 → javac 編譯 → .class Bytecode → JVM 執行 → 機器碼

JVM 記憶體架構概覽


Heap vs Stack 核心差異

Stack(堆疊)

用途:儲存方法執行期間的區域變數和方法呼叫資訊

特性

  • 執行緒專屬:每個執行緒有自己的 Stack
  • 自動管理:方法結束後自動清除
  • 速度快:存取速度快(LIFO,後進先出)
  • 大小固定:預設 1MB(可調整)
  • 儲存內容
    • 基本型別(int, long, double, boolean 等)的
    • 物件的參考(reference)
    • 方法參數
    • 區域變數

範例

public void calculateSum() {
    int a = 10;        // a 存在 Stack
    int b = 20;        // b 存在 Stack
    int sum = a + b;   // sum 存在 Stack
}
// 方法結束,Stack 自動清空 a, b, sum

StackOverflowError

  • 發生原因:遞迴太深、Stack 空間不足
public void recursion() {
    recursion();  // 無限遞迴,最終拋出 StackOverflowError
}

Heap(堆積)

用途:儲存所有物件實例和陣列

特性

  • 所有執行緒共享:全域共用的記憶體空間
  • 需要 GC 管理:垃圾回收器負責清理
  • 速度較慢:相對於 Stack
  • 大小可調整:啟動時可設定
  • 儲存內容
    • 所有 new 出來的物件
    • 物件的成員變數
    • 陣列

範例

public void createObject() {
    Person person = new Person("Alice");
    // person(參考)在 Stack
    // new Person()(物件實例)在 Heap
}

Heap vs Stack 快速對照

項目 Stack Heap
用途 方法執行資訊、區域變數 物件實例、陣列
共享 執行緒專屬 所有執行緒共享
速度 較慢
管理 自動(方法結束即清除) GC 管理
大小 小(預設 ~1MB) 大(預設數百 MB 到數 GB)
錯誤 StackOverflowError OutOfMemoryError
儲存 基本型別值、參考 物件、陣列

記憶體分配範例

範例 1:基本型別 vs 物件

public void example() {
    // Stack:值直接存在 Stack
    int age = 30;
    boolean isActive = true;

    // Heap + Stack
    String name = new String("Alice");
    // name(參考)→ Stack
    // "Alice" 物件 → Heap

    Person person = new Person("Bob", 25);
    // person(參考)→ Stack
    // Person 物件 → Heap
}

記憶體示意圖

Stack                     Heap
┌─────────────┐          ┌──────────────────┐
│ age: 30     │          │ String("Alice")  │ ← name 指向
│ isActive:   │          │                  │
│   true      │          │ Person {         │ ← person 指向
│ name: 0x100 │ ────────>│   name: "Bob"    │
│ person:     │          │   age: 25        │
│   0x200     │ ────────>│ }                │
└─────────────┘          └──────────────────┘

範例 2:方法呼叫

public class Example {
    public static void main(String[] args) {
        int x = 10;
        processData(x);
    }

    public static void processData(int value) {
        String message = "Processing";
        int result = value * 2;
    }
}

Stack 變化

1. main() 進入
   Stack: [x=10]

2. processData() 被呼叫
   Stack: [x=10] → [value=10, message(參考), result=20]

3. processData() 結束
   Stack: [x=10]  (processData 的資料被清除)

4. main() 結束
   Stack: []      (清空)

Heap 記憶體詳細結構

Heap 分代(Generational)架構

Heap
├─ Young Generation(年輕代)- 新物件
│  ├─ Eden Space(伊甸園)- 新物件首次分配
│  └─ Survivor Space(倖存者空間)
│     ├─ S0(From)
│     └─ S1(To)
└─ Old Generation(老年代)- 長期存活的物件

Young Generation(年輕代)

Eden Space

  • 所有新物件最初分配的地方
  • 大部分物件生命週期短,很快就被回收

Survivor Space (S0, S1)

  • 從 Eden 存活下來的物件會移到這裡
  • S0 和 S1 來回交替使用
  • 經過多次 Minor GC 仍存活 → 晉升到 Old Generation

Minor GC(輕量 GC)

  • 只清理 Young Generation
  • 頻率高、速度快
  • 影響較小

Old Generation(老年代)

存放物件

  • 長期存活的物件
  • 大型物件(直接分配)

Major GC / Full GC(重量 GC)

  • 清理 Old Generation
  • 頻率低、速度慢
  • 影響大(Stop The World)

物件生命週期

1. 新物件創建 → Eden Space

2. Eden 滿了 → Minor GC
   ├─ 存活的物件 → Survivor Space (S0)
   └─ 死亡的物件 → 回收

3. 再次 Minor GC
   ├─ Eden + S0 存活的 → S1
   └─ 死亡的 → 回收

4. 多次 GC 後仍存活(通常 15 次)
   → 晉升到 Old Generation

5. Old Generation 滿了 → Major GC / Full GC

Method Area(方法區 / Metaspace)

Java 7 之前:PermGen(永久代)

儲存內容

  • 類別資訊(Class metadata)
  • 靜態變數
  • 常量池(Constant Pool)
  • 方法資訊

問題

  • 大小固定,容易 OutOfMemoryError: PermGen space
  • 調整困難

Java 8+ : Metaspace(元空間)

改進

  • 使用原生記憶體(Native Memory),不再受 Heap 限制
  • 自動擴展,預設無上限
  • 減少 OutOfMemoryError: PermGen space 錯誤

儲存內容

  • 類別 metadata
  • 方法資訊
  • 常量池

設定參數

-XX:MetaspaceSize=128m        # 初始大小
-XX:MaxMetaspaceSize=256m     # 最大大小

垃圾回收(Garbage Collection, GC)

什麼是 GC?

自動記憶體管理機制,回收不再使用的物件,釋放記憶體。


GC 如何判斷物件可以回收?

1. 引用計數(Reference Counting)- 不採用

每個物件維護引用數量,為 0 時回收。

問題:無法處理循環引用

ObjectA.ref = ObjectB;
ObjectB.ref = ObjectA;
// 兩者引用數都不為 0,但實際上都無法被訪問

2. 可達性分析(Reachability Analysis)- Java 採用

GC Roots 開始,標記所有可達物件,不可達的物件就是垃圾。

GC Roots 包括

  • Stack 中的局部變數
  • 靜態變數
  • 常量池中的引用
  • JNI 引用

常見 GC 演算法

演算法 說明 優點 缺點
Serial GC 單執行緒 GC 簡單 暫停時間長
Parallel GC 多執行緒 GC 吞吐量高 暫停時間長
CMS GC 並發標記清除 暫停時間短 碎片化
G1 GC 分區回收 平衡吞吐量和延遲 複雜
ZGC 超低延遲 暫停 < 10ms 需要大記憶體

G1 GC(Garbage First)

Java 9+ 預設 GC

特點

  • 將 Heap 分成多個小區域(Region)
  • 優先回收垃圾最多的區域
  • 可預測的暫停時間

適用情境

  • 大型 Heap(> 4GB)
  • 需要可預測的低延遲

JVM 記憶體參數設定

常用參數

# Heap 大小設定
-Xms2g          # 初始 Heap 大小(2GB)
-Xmx4g          # 最大 Heap 大小(4GB)

# Young Generation 設定
-Xmn1g          # Young Generation 大小(1GB)
-XX:NewRatio=2  # Old:Young 比例(2:1)

# Stack 大小設定
-Xss1m          # 每個執行緒 Stack 大小(1MB)

# Metaspace 設定
-XX:MetaspaceSize=128m      # 初始 Metaspace 大小
-XX:MaxMetaspaceSize=256m   # 最大 Metaspace 大小

# GC 選擇
-XX:+UseG1GC              # 使用 G1 GC
-XX:+UseSerialGC          # 使用 Serial GC
-XX:+UseParallelGC        # 使用 Parallel GC
-XX:+UseConcMarkSweepGC   # 使用 CMS GC

# GC 日誌
-XX:+PrintGCDetails       # 列印 GC 詳細資訊
-XX:+PrintGCDateStamps    # GC 時間戳記
-Xloggc:/path/to/gc.log   # GC 日誌檔案位置

# OutOfMemoryError 時產生 Heap Dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dumps

範例:啟動應用程式

java -Xms2g -Xmx4g -Xss1m \
     -XX:+UseG1GC \
     -XX:+PrintGCDetails \
     -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/tmp/heapdump.hprof \
     -jar myapp.jar

常見記憶體問題

1. OutOfMemoryError: Java heap space

原因:Heap 記憶體不足

解決方式

# 增加 Heap 大小
-Xmx4g

# 或檢查記憶體洩漏(Memory Leak)

如何檢查

# 使用 jmap 產生 Heap Dump
jmap -dump:live,format=b,file=heap.hprof <pid>

# 使用 MAT(Eclipse Memory Analyzer)分析

2. OutOfMemoryError: PermGen space(Java 7)

原因:PermGen 空間不足(類別太多)

解決方式

# Java 7
-XX:PermSize=128m
-XX:MaxPermSize=256m

# Java 8+ 改用 Metaspace
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m

3. StackOverflowError

原因:Stack 空間不足(通常是遞迴太深)

解決方式

# 增加 Stack 大小(治標)
-Xss2m

# 修正程式碼(治本)
# - 避免無限遞迴
# - 改用迴圈
# - 使用尾遞迴優化

4. 記憶體洩漏(Memory Leak)

常見原因

  • 靜態集合持有物件參考
  • 未關閉的資源(檔案、連線)
  • 監聽器未移除
  • ThreadLocal 未清理

範例(記憶體洩漏)

public class LeakExample {
    private static List<byte[]> list = new ArrayList<>();

    public void addData() {
        // 靜態 list 不斷增長,物件無法被 GC
        list.add(new byte[1024 * 1024]); // 1MB
    }
}

修正

public class FixedExample {
    private List<byte[]> list = new ArrayList<>();

    public void addData() {
        list.add(new byte[1024 * 1024]);
    }

    public void cleanup() {
        list.clear();  // 清除參考
    }
}

記憶體監控工具

1. jps - 查看 Java 進程

# 列出所有 Java 進程
jps -l

# 輸出範例:
# 12345 com.example.MyApplication
# 12346 org.apache.tomcat.Bootstrap

2. jstat - JVM 統計資訊

# 查看 GC 統計(每 1000ms 更新一次)
jstat -gc <pid> 1000

# 查看 Heap 使用情況
jstat -gccapacity <pid>

# 查看 GC 原因
jstat -gccause <pid>

輸出範例

S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU
10240  10240  0      8192   81920    40960   163840     81920   51200  48128

說明:

  • S0C/S1C:Survivor 0/1 容量
  • EC:Eden 容量
  • EU:Eden 使用量
  • OC:Old Generation 容量
  • OU:Old Generation 使用量
  • MC:Metaspace 容量
  • MU:Metaspace 使用量

3. jmap - Heap Dump

# 產生 Heap Dump
jmap -dump:live,format=b,file=heap.hprof <pid>

# 查看 Heap 使用摘要
jmap -heap <pid>

# 查看物件統計
jmap -histo <pid>

4. jstack - 執行緒堆疊

# 產生執行緒堆疊快照
jstack <pid> > thread_dump.txt

# 偵測死鎖
jstack -l <pid>

5. VisualVM - 圖形化監控

# 啟動 VisualVM(JDK 自帶)
jvisualvm

功能

  • 即時監控 CPU、記憶體、執行緒
  • Heap Dump 分析
  • 執行緒分析
  • Profiling

6. MAT (Eclipse Memory Analyzer Tool)

用途:分析 Heap Dump,找出記憶體洩漏

下載https://eclipse.dev/mat/

功能

  • Leak Suspects(洩漏懷疑)報告
  • Dominator Tree(支配樹)
  • OQL(物件查詢語言)

效能調校建議

1. 合理設定 Heap 大小

# 原則:-Xms 和 -Xmx 設為相同,避免 Heap 動態調整
-Xms4g -Xmx4g

# 不要設太小(頻繁 GC)
-Xmx512m  # ❌ 太小

# 不要設太大(GC 暫停時間長)
-Xmx64g   # ⚠️ 考慮使用 ZGC

2. 選擇合適的 GC

應用場景 推薦 GC
小型應用(< 1GB Heap) Serial GC
批次處理(追求吞吐量) Parallel GC
低延遲應用(< 4GB Heap) CMS GC
一般應用(> 4GB Heap) G1 GC(預設)
超低延遲(> 8GB Heap) ZGC / Shenandoah

3. 避免記憶體洩漏

// ✅ 使用 try-with-resources 自動關閉資源
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // 使用 fis
} // 自動關閉

// ✅ 移除監聽器
button.removeActionListener(listener);

// ✅ 清理 ThreadLocal
threadLocal.remove();

// ✅ 使用 WeakReference 避免強引用
WeakReference<MyObject> weakRef = new WeakReference<>(obj);

4. 物件池(Object Pooling)

對於頻繁創建和銷毀的物件:

// 使用 Apache Commons Pool
GenericObjectPool<MyObject> pool = new GenericObjectPool<>(factory);

MyObject obj = pool.borrowObject();
try {
    // 使用 obj
} finally {
    pool.returnObject(obj);
}

快速檢查清單

記憶體問題排查步驟

  1. 確認問題類型

    # 查看錯誤訊息
    OutOfMemoryError: Java heap space → Heap 不足
    StackOverflowError → Stack 不足(遞迴問題)
    
  2. 檢查記憶體使用

    jstat -gc <pid> 1000
    
  3. 產生 Heap Dump

    jmap -dump:live,format=b,file=heap.hprof <pid>
    
  4. 分析 Heap Dump

    • 使用 MAT 或 VisualVM
    • 查找大物件和記憶體洩漏
  5. 調整 JVM 參數

    -Xms4g -Xmx4g -XX:+UseG1GC
    

總結速查表

記憶體區域對照

區域 儲存內容 共享 管理 大小
Heap 物件、陣列 共享 GC
Stack 區域變數、參考 執行緒專屬 自動
Metaspace 類別資訊 共享 自動擴展

關鍵 JVM 參數

-Xms2g -Xmx4g              # Heap 大小
-Xss1m                     # Stack 大小
-XX:+UseG1GC               # 使用 G1 GC
-XX:MetaspaceSize=128m     # Metaspace 大小
-XX:+HeapDumpOnOutOfMemoryError  # OOM 時產生 Heap Dump

常用監控命令

jps -l                     # 查看 Java 進程
jstat -gc <pid> 1000       # 監控 GC
jmap -heap <pid>           # 查看 Heap 資訊
jstack <pid>               # 查看執行緒堆疊
jmap -dump:live,format=b,file=heap.hprof <pid>  # Heap Dump

記憶體最佳實踐

  1. 合理設定 Heap-Xms = -Xmx
  2. 選擇合適 GC:一般用 G1 GC
  3. 監控記憶體:定期檢查 GC 日誌
  4. 避免洩漏:關閉資源、清理參考
  5. 分析 Dump:使用 MAT 找出問題

實用範例

完整的 JVM 啟動配置

#!/bin/bash

JAVA_OPTS="
  -server
  -Xms4g
  -Xmx4g
  -Xss1m
  -XX:+UseG1GC
  -XX:MaxGCPauseMillis=200
  -XX:MetaspaceSize=256m
  -XX:MaxMetaspaceSize=512m
  -XX:+HeapDumpOnOutOfMemoryError
  -XX:HeapDumpPath=/var/log/myapp/heapdump.hprof
  -XX:+PrintGCDetails
  -XX:+PrintGCDateStamps
  -Xloggc:/var/log/myapp/gc.log
  -XX:+UseGCLogFileRotation
  -XX:NumberOfGCLogFiles=5
  -XX:GCLogFileSize=20M
"

java $JAVA_OPTS -jar myapp.jar

建立日期:2024-11-05 最後更新:2025-11-18

🔗相關文章