堆與棧是 JVM 內存模型的兩大支柱,混淆二者是導致記憶體問題排查失效的首因。本文從內存佈局、對象分配規則、異常特徵、參數控制、實戰排查五個維度徹底釐清兩者邊界。
| 維度 | 堆 | 棧 |
|---|---|---|
| 共享性 | 所有執行緒共享 | 執行緒私有 |
| 存儲內容 | 所有 new 出來的對象實例、陣列 | 基本型別局部變數、對象引用、方法調用幀、操作數棧 |
| 生命週期管理 | GC (標記-清理/複製/標記-整理) | 方法進棧/出棧自動釋放 |
| 大小控制參數 | -Xms (初始)、-Xmx (最大) | -Xss (每執行緒棧大小,預設 1MB) |
| 典型異常 | OutOfMemoryError: Java heap space | StackOverflowError |
| 擴展性 | 動態擴展至 -Xmx | 固定大小,不可動態擴展 |
| 存取速度 | 較慢 (需 GC 掃描、壓縮) | 極快 (指針遞增/遞減) |
void demo() {
User user = new User("Alice"); // 1. 棧分配引用變數 user
// 2. 堆分配 User 對象
// 3. user 指向堆地址
}
int, long 等) 直接存棧JIT (C2) 啟用逃逸分析 (-XX:+DoEscapeAnalysis,預設開啟) 後:
// 典型無逃逸場景
public String concat(String a, String b) {
StringBuilder sb = new StringBuilder(); // 無逃逸
sb.append(a).append(b);
return sb.toString(); // 返回 String,sb 不逃逸
}
// JIT 可優化為棧上分配甚至直接字串拼接
String.intern() 易爆 PermGenintern() 安全# 查看預設值
java -XX:+PrintFlagsFinal -version | grep -E 'HeapSize|ThreadStackSize'
# 運行時動態查看
jmap -heap
jcmd VM.flags
jstat -gcutil 1000
# 典型生產配置 (8GB 容器)
-Xms4g -Xmx4g -Xss1m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
發生 OOM │ ├─ Heap OOM → jmap -histo:live| head -30 │ → 定位佔用最大類 → MAT 分析 GC Roots 路徑 → 修復洩漏/調大 -Xmx │ ├─ Metaspace OOM → 類加載洩漏 (CGLib/JSP/Groovy 動態生成類) │ → -XX:MaxMetaspaceSize 限制、檢查類加載器洩漏 │ ├─ Direct Memory OOM → NIO/Netty DirectByteBuffer 未釋放 │ → -XX:MaxDirectMemorySize、排查 Cleaner/ReferenceQueue │ └─ StackOverflow → 遞歸無出口 / 大局部變數陣列 / 循環調用 → -Xss 調大 (治標) / 重寫遞歸為迴圈 (治本)
String s = "a" + "b" 在堆創建對象 → 編譯期常量折疊,直接指向字串常量池-Xmx 只限制堆 → 進程總記憶體 = Heap + Metaspace + Stack×Threads + Direct + CodeCache + GC 開銷-XX:InitialRAMPercentage/MaxRAMPercentage 讓 JVM 感知容器記憶體上限-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps 自動留存現場堆是「對象的家」,棧是「方法的工作台」。搞清誰住哪、誰管命週期、誰爆了怎麼查,是 Java 工程師從「會用」走向「懂原理」的分水嶺。下次遇到 OOM/StackOverflow,請先問自己:**是堆滿了,還是棧溢出了?**