彻底搞懂 MAT:Shallow Heap 与 Retained Heap 的底层算法与性能调优实战
在 Java 性能调优的战场上,Eclipse MAT (Memory Analyzer Tool) 是每一位开发者分析堆转储(Heap Dump)的利器。然而,面对 MAT 报告中两个最基础的指标——Shallow Heap 与 Retained Heap,很多开发者仅仅停留在“一个是自己,一个是总和”的模糊认知上。
本文将深入 JVM 底层与图论算法,深度剖析这两个指标的计算逻辑,并结合实际场景聊聊它们在定位内存泄漏中的核心价值。
一、 Shallow Heap:对象自身的“净重”
Shallow Heap 指的是对象本身在堆内存中占用的空间大小,不包含其引用的其他对象。
1. 计算组成
一个 Java 对象的 Shallow Heap 主要由以下三部分组成:
- 对象头 (Object Header): 包含 Mark Word(存储哈希码、锁状态等)和 Class Pointer(指向元空间的类元数据)。在 64 位 JVM 中,开启指针压缩(CompressedOops)通常占用 12 字节。
- 实例数据 (Instance Data): 对象中定义的各种成员变量。原生类型(int, long, boolean 等)按其实际大小计算,引用类型(Reference)在开启指针压缩时占用 4 字节。
- 对齐填充 (Padding): JVM 要求对象起始地址必须是 8 字节的整数倍。如果前两部分之和不是 8 的倍数,则会进行填充。
2. 特点
- 固定性: 对于同一种类的两个对象,它们的 Shallow Heap 通常是完全相同的。
- 数组例外: 数组对象的 Shallow Heap 取决于数组的长度和元素类型。
二、 Retained Heap:对象被回收后的“红利”
Retained Heap 是一个更具实战意义的指标。它表示如果该对象被垃圾回收器(GC)回收,堆内存中总共能释放出来的空间大小。
1. 核心逻辑:支配树 (Dominator Tree)
Retained Heap 的计算基于图论中的**支配(Domination)**概念。
如果从 GC Root 到对象 B 的每一条路径都必须经过对象 A,那么我们说 A 支配 B。
Retained Heap = 对象 A 的 Shallow Heap + 所有由 A 直接或间接支配的对象(即该对象被回收时,也会跟着被回收的对象)的 Shallow Heap 之和。
2. 深度理解
想象一个场景:对象 A 引用了对象 B,而对象 C 也引用了对象 B。
- 此时,如果 A 被回收,B 并不会被回收(因为 C 还拽着它)。
- 因此,B 的内存不属于 A 的 Retained Heap。
- 只有当 A 是 B 走向 GC Root 的唯一必经之路时,B 才会贡献给 A 的 Retained Heap。
三、 案例演算:以 String 和 ArrayList 为例
为了直观理解,我们看两个典型的结构:
1. String 对象
一个 java.lang.String 对象包含一个 char[] 数组。
- Shallow Heap: 仅包含 String 对象头、指向 char 数组的引用字段和 hash 字段,通常为 24 或 32 字节。
- Retained Heap: String 自身的 Shallow Heap +
char[]数组的 Shallow Heap。因为通常情况下,char[]是由 String 私有的,String 挂了,数组也就挂了。
2. 共享数据
如果有两个 String 对象 $S_1$ 和 $S_2$,由于字符串常量池或特殊逻辑,它们共用了同一个 char[]。
- 此时,$S_1$ 的 Retained Heap 就不包含该
char[]的大小。 - 这对分析内存瓶颈非常有参考价值:如果你发现一个大对象的 Retained Heap 竟然等于 Shallow Heap,说明它持有的子对象正在被其他地方引用,你可能找错了优化目标。
四、 实战应用:如何利用指标定位问题
在 MAT 的 Dominator Tree 视图中,我们通常按 Retained Heap 进行倒序排列。
1. 揪出“内存吞噬者”
Retained Heap 最大的对象往往就是内存泄漏的根源(即 Leak Suspect)。即使一个对象的 Shallow Heap 很小(例如一个空的 HashMap 容器),如果它持有了数百万个子对象,它的 Retained Heap 会惊人地大。
2. 识别“虚假大对象”
有时你会发现一个对象 A 引用了很多数据,但它的 Retained Heap 却很小。这说明 A 持有的对象被其他活跃对象共享了。
- 场景: 在复杂的缓存框架(如 Caffeine, Ehcache)中,这是常态。
- 对策: 你需要查看 Incoming References,找出谁还在持有这些数据。
3. 优化集合类
如果通过 MAT 发现某个 ArrayList 的 Shallow Heap 与 Retained Heap 差值巨大,但其中很多元素其实是重复的引用,这可能提示你应该使用更高效的数据结构,或者考虑 intern() 机制来减少内存占用。
五、 总结
- Shallow Heap 告诉你这个对象“长多胖”。
- Retained Heap 告诉你这个对象“管多宽”。
在处理 OOM 问题时,优先关注 Retained Heap。它不仅体现了对象的占用,更揭示了对象间的支配关系。通过 MAT 的支配树,我们可以顺藤摸瓜,找到那个导致内存溢出的“罪魁祸首”。
提示: 在分析大型 Dump 文件时,建议先执行 Discard secondary references 以排除干扰,并重点分析那些 Retained Heap 异常庞大的根节点。