前言:為什麼要搞懂 Shallow Heap 與 Retained Heap?
在使用 Eclipse MAT (Memory Analyzer Tool) 進行記憶體洩漏分析時,Shallow Heap 與 Retained Heap 是兩個最核心、也是最容易混淆的指標。搞清楚它們的定義與差異,是精準定位記憶體佔用大戶、解決 OOM (OutOfMemoryError) 問題的基礎。
一、Shallow Heap(淺堆):對象自身的「體重」
定義: 對象自身占用的記憶體大小,不包含 它引用的其他對象。
- 普通對象: 大小 = 對象頭 + 實例字段 (含引用指標) + 對齊填充。引用類型字段佔用 4 或 8 位元組 (視 JVM 位數與壓縮指標開關),儲存的是指向堆中真實對象的地址,而非對象本身。
- 陣列對象: 大小 = 陣列頭 + 陣列元素總大小。元素若為基本類型則直接儲存值,若為引用類型則儲存引用指標。
通俗理解: 這是對象「脫離關係」後的獨立體重,即 sizeof(object)。
二、Retained Heap(保留堆):對象的「真實影響力」
定義: Retained Heap = 當前對象 Shallow Heap + 當前對象直接或間接引用的所有對象的 Shallow Heap 總和,前提是這些被引用對象沒有被 GC Roots 或其他強引用路徑引用。
核心意義: 如果該對象被 GC 回收,堆上總共能釋放多少記憶體。這才是衡量對象「佔用記憶體真實大小」的關鍵指標。
三、圖解 Retained Heap 計算邏輯
假設 GC Roots 直接引用 A 和 B。A 無引用,B 引用 C,C 引用 D。但 D 同時也被 GC Roots 直接引用。
- A 的 Retained Heap = A.Shallow (無下游對象)
- B 的 Retained Heap = B.Shallow + C.Shallow
- D 不計入 B 的 Retained Heap,因為 D 被 GC Roots 直接引用,屬於「共享引用」,B 死不代表 D 死。
若移除 GC Roots 對 D 的直接引用:
- 此時 B 的 Retained Heap = B.Shallow + C.Shallow + D.Shallow
- 因為 D 唯一的強引用路徑來自 B -> C -> D,B 被回收時,C、D 連帶回收。
四、實戰建議
- 排查大對象看 Shallow Heap:定位單個巨大對象 (如巨大 byte[]、char[])。
- 排查記憶體洩漏看 Retained Heap:按 Retained Heap 降序排列,頂層往往是洩漏源頭 (如靜態集合、單例持有的大量對象)。
- 注意支配者樹:MAT 的 Dominator Tree 正是基於 Retained Heap 邏輯構建,直接展示「誰支配誰」,是分析的利器。
總結: Shallow Heap 是「自身體重」,Retained Heap 是「連帶責任」。分析記憶體時,Retained Heap 才是王道。