WEBKT

彻底搞懂 MAT:Shallow Heap 与 Retained Heap 的底层算法与性能调优实战

4 0 0 0

在 Java 性能调优的战场上,Eclipse MAT (Memory Analyzer Tool) 是每一位开发者分析堆转储(Heap Dump)的利器。然而,面对 MAT 报告中两个最基础的指标——Shallow HeapRetained 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 异常庞大的根节点。

码农老姜 JVM 调优MAT内存分析

评论点评