Java 自動記憶體管理雖減輕開發者負擔,但不理解堆與棧的存儲差異、物件生命週期、GC 行為,極易遭遇 OutOfMemoryError 或效能抖動。本文系統解析記憶體模型、參數調優、OOM 類型診斷、String 常量池機制,並提供生產級排查工具鏈。
| 維度 | 堆 | 棧 |
|---|---|---|
| 存儲內容 | 所有物件實例、陣列 | 基本型別區域變數、物件參考、方法幀、返回地址 |
| 配置方式 | 動態(new)、GC 管理 | 靜態(編譯期決定大小)、線程私有、自動釋放 |
| 存取速度 | 較慢(需指標間接存取) | 極快(僅次於 CPU 寄存器) |
| 共享性 | 線程共享 | 線程私有 |
| 彈性 | 大小不固定、生命週期動態 | 大小固定、生命週期跟隨作用域 |
| 錯誤類型 | OutOfMemoryError: Java heap space | StackOverflowError |
| 參數 | 含義 | 建議 |
|---|---|---|
-Xms | 初始堆大小 | 設定與 -Xmx 相同,避免擴縮容震盪 |
-Xmx | 最大堆大小 | 物理記憶體 60%-80%,預留 OS、Metaspace、CodeCache、線程棧 |
-Xmn | 新生代大小 | 通常設為堆 1/3~1/2,配合 GC 策略調整 |
-Xss | 線程棧大小 | 256k~1M,遞歸深度大時調大,線程數極多時調小 |
-XX:MetaspaceSize/MaxMetaspaceSize | 元空間大小 | 類載入量大的應用需顯式設定,防止 Metaspace OOM |
# JVM 參數 (catalina.sh / eclipse.ini / 啟動腳本) -Xms2g -Xmx2g -Xmn1g -Xss512k -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/heapdump.hprof
Runtime rt = Runtime.getRuntime(); long max = rt.maxMemory(); // -Xmx long total = rt.totalMemory(); // 當前已向 OS 申請 long free = rt.freeMemory(); // 當前可用 long used = total - free; // 實際佔用
-XX:PretenureSizeThreshold)直接進老年代。-XX:MaxTenuringThreshold,預設 15)晉升老年代。| 錯誤訊息 | 原因 | 排查方向 |
|---|---|---|
| java.lang.OutOfMemoryError: Java heap space | 堆耗盡,無法分配新物件 | 1. 檢查記憶體洩漏(集合未清理、靜態引用、監聽器未移除) 2. 堆過小,調大 -Xmx 3. 大物件分配(大報表、大文件讀取)改用流式處理 |
| java.lang.OutOfMemoryError: Metaspace | 類元數據區耗盡 | 1. 類載入過多(動態代碼生成、JSP、Groovy、大量代理類) 2. 設定 -XX:MaxMetaspaceSize 3. 檢查類加載器洩漏(熱部署、OSGi、Tomcat 重啟未清理) |
| java.lang.OutOfMemoryError: unable to create new native thread | 本地記憶體不足以建立線程 | 1. 線程池未關閉/無上限 2. 減小 -Xss 3. 升級 64 位 JVM / 增加物理記憶體 |
| java.lang.OutOfMemoryError: Requested array size exceeds VM limit | 單一陣列過大超過限制 | 檢查陣列分配邏輯,分批處理 |
String s1 = "abc"; // 檢查常量池,無則建立並入池,參考指向池中物件
String s2 = "abc"; // 直接指向池中同一物件 → s1 == s2 為 true
String s3 = new String("abc"); // 堆中建立新物件,內容複製自常量池 → s1 == s3 為 false
String s4 = new String("abc"); // 又一個堆物件 → s3 == s4 為 false
String s5 = new String("abc").intern(); // 若池有 "abc" 回傳池引用,否則放入池並回傳
// s5 == s1 → true
JDK 7 起常量池移至堆中,String.intern() 效能大幅提升,但大量調用仍可能導致 Metaspace/堆壓力。
StringBuilder/StringBuffer(避免產生大量中間物件)。new String()。enum 或 intern() 節省記憶體。jmap -dump:live,format=b,file=heap.hprof <pid> → MAT (Eclipse Memory Analyzer) / JHat / VisualVM 分析支配者樹、洩漏疑點。-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags (JDK 9+) → GCeasy、GCEasy.io、GCViewer 可視化。jstack <pid> > threaddump.txt 分析死鎖、CPU 高佔用線程。== 與 equals() 行為,慎用 new String()。