写在前面的话
最近在排查各种性能问题。又用到了好多年没有使用的mat。 问题排查-内存溢出
Heap 名词概念
Eclipse MAT(内存分析器工具)是分析 JVM 堆 Dump 文件的强大工具。当尝试分析内存相关的问题时,它非常方便。在 Eclipse MAT 内存分析的报告中会显示对象两种类型的 Heap 信息:
- Shallow heap
- Retained heap
我们主要讨论它们之间的区别,并探讨它们的计算方式。
例如,假设你的应用程序具有这样的对象模型,如图 1 (SH指 shallow heap, b 指的是 byte):
图1:内存中的对象
- 对象 A 持有对象 B 和 C 的引用。
- 对象 B 持有对象 D 和 E 的引用。
- 对象 C 持有对象 F 和 G 的引用。
另外,我们假设每个对象占用 10 个字节的内存。在这种场景下,我们开始分析Heap大小的过程。
Shallow Heap
对象的 Shallow heap 是其自身在内存中的大小。说的直白点就是,在没有引用其他对象的情况下本身占用的内存大小,不包括它引用的对象。
- 针对数组类型(数组或集合)的对象,它的大小是数组元素对象的大小总和。
- 针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。
由于在我们的示例中,每个对象占用大约 10 个字节,因此每个对象的 Shallow heap 大小为 10 个字节。
Retained Heap
Retained heap: 当对象不再被引用时,垃圾回收器所能回收的总内存,包括对象自身所占据的内存,以及仅能够通过该对象引用到的其他对象所占据的内存: 对象本身的大小 + 引用的其他对象的大小。
换句话说,Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存。
B 的 Retained Heap 大小
从图 1 中,可以注意到对象 B 持有对象 D 和 E 的引用。因此,如果对象 B 是从内存中被垃圾回收,则将不再有对对象 D 和 E 的引用。这意味着此时 D 和 E 也可以被垃圾收集。
Retained heap 指的就是在垃圾回收特定对象时将释放的内存量。
因此,B 的保留堆大小为:= B 的 shallow heap 大小 + D 的 shallow heap 大小 + E 的 shallow heap 大小 = 10 bytes + 10 bytes + 10 bytes = 30 bytes。因此,B 对象的 Retained heap 大小为 30 字节
C 的 Retained Heap 大小
对象 C 拥有对象 F 和 G 的引用。如果对象 C 是从内存中垃圾回收的,将不再持有对对象 F 和 G 的引用。这意味着此时 F 和 G 也可以被垃圾回收。因此,C 的 Retained Heap 大小为:= C 的 shallow heap 大小 + F 的 shallow heap 大小 + G 的 shallow heap 大小 = 10 bytes + 10 bytes + 10 bytes = 30 bytes。因此,C 对象的 Retained heap 大小为 30 字节。
A 的 Retained Heap 大小
对象 A 持有对象 B 和 C 的引用,而对象 B 和 C 又持有对对象 D、E 以及 F、G 的引用。因此,如果对象 A 是从内存中垃圾回收的,则将不再有对 B、C、D、E、F 和 G 对象的引用。
基于此理解,我们来计算下 A 的 Retained Heap 大小。
A 的 Retained Heap 大小为:= A 的 shallow heap 大小 + B 的 shallow heap 大小 + C 的 shallow heap 大小 + D 的 shallow heap 大小 + E 的 shallow heap 大小 + F 的 shallow heap 大小 + G 的 shallow heap 大小 = 10 bytes + 10 bytes + 10 bytes + 10 bytes + 10 bytes + 10 bytes + 10 bytes = 70 bytes。
最后我们可以得出,A 的 Retained heap 大小是 70 字节。
D、E、F、G 的 Retained Heap 大小
D 的 Retained heap 大小与其 Shallow heap 大小相同,就是 10 个字节,因为 D 不持有对任何其他对象的引用。因此,如果 D 获得了垃圾回收,则不会从内存中删除其他的任何对象。同理,E、F 和 G 的 Retained heap 大小也只有 10 个字节。
图 2:对象的 Shallow and Retained Heap 大小(SH指 shallow heap, RH指 Retained heap,b 指的是 byte)
多一些引用的变数
为了进一步加深对 Shallow heap 和 Retained heap 的理解。在下面的示例中,让对象 H 开始持有对 B 的引用(注意:对象 B 已经被对象 A 引用了)。现在 A 和 H 都持有对象 B 的引用。在这种情况下, Retained heap 计算逻辑将会发生什么变化呢?
图3: 新增对 B 的引用
在这种情况下,对象 A 的 Retained heap 大小将从之前的 70 减小到 40 个字节。为什么?
如果对象 A 被垃圾回收了,则将仅会影响 C、F 和 G 对象的引用。因此,仅对象 C、F 和 G 将被垃圾回收。
另一方面,由于 H 持有对 B 的活动引用,因此对象 B、D 和 E 将继续存在于内存中。因此,即使 A 被垃圾回收,B、D 和 E 也不会从内存中删除。
于是可以计算出, A 的 Retained heap 大小为:= A 的 shallow heap 大小 + C 的 shallow heap 大小 + F 的 shallow heap 大小 + G 的 shallow heap 大小 = 10 bytes + 10 bytes + 10 bytes + 10 bytes = 40 bytes。
A 的 Retained heap 大小将变为 40 个字节。所有其他对象 Retained heap 大小将保持不变,因为它们的引用没有变化。
其他名词
out going(查看对象为什么消耗内存, 查看对象引用的其他对象)
表示的是当前对象,引用了外部对象
可以使用右键 with outgoing references, 此类对象持有的其他对象。在 Attributes 中可以看到观察对象持有的成员。
in going(查看对象被谁引用)
表示的是当前查看的对象,被外部应用。
可以使用右键 with ingoing references, 其他对象持有的此类对象。
path to GC root (对象没被释放掉的引用)
到GC root的路径
这里有一个特殊的节点GC Roots,这就是reference chain的起点。
Merge Shortest path to GC root (对象没被释放掉的引用)
到GC root的最短路径,右键 merge shortest path to gc root
-> exclude all phantim/weak/soft etc. references
:查看此对象没被释放掉的原因,只保留强引用。
Histogram
堆内所有类的统计信息,包含类的实例数量和占用的空间。如果此处包含了自己的类就需要注意是否此类创建过多。
默认的大小单位是 Bytes,可以在 Window – Preferences—Memory Analyzer-- 菜单中设置单位。
可以通过filter搜索出自己的类,可以看到内存中共有32049817个Column对象,占用了13GB内存。
Dominator Tree
列出了堆中最大的对象,并且引用此对象的其他对象。
如上图,可以很清晰的看到最大的对象是 XXXService @ 0x7fd1752c6f20 这个对象占用了大量内存,次对象被类加载器所引用。
Top Consumers
按类、对象、包分组,列出最消耗资源的类、对象、包。
Duplicate Classes
对多个类加载器加载的类进行分析。
Leak Suspects
内存泄漏报告和系统概述
Top Components
列出内存用量超过堆总量1%的组件。