分析和了解应用程序的内存使用情况是一项具有挑战性的操作。细微的 logic 错误可能导致侦听器永远不会被处理,最终导致可怕的 OutOfMemory 错误。即使您的应用程序正确处理了所有未使用的对象,它仍可能需要比所需多 10 倍或 100 倍的内存。
幸运的是,Eclipse Memory Analyzer (MAT) 可以帮助提供应用程序内存使用情况的详细信息。该工具可用于跟踪内存泄漏和定期查看系统状态。在本教程中,我将概述 10 个技巧来帮助您更有效地使用 MAT。如果您是 Java 开发人员,Eclipse Memory Analyzer Tool 当然应该在您的调试工具箱中。
在这个例子中,我们使用的是一个非常简单的程序,它分配了 100,000 个 Listener,并将它们存储在 4 个不同的列表中。然后,应用程序将休眠,而不删除或清除这些列表。
1、获取内存快照(Heap Dump)
您可以通过多种方式开始使用 MAT
● 将应用程序配置为在发生内存不足错误时转储其内存内容,
● 将 MAT 连接到现有的 java 进程,或者
● 手动获取堆转储并将其加载到 MAT 中
在所有情况下,请务必记住,这是某个时间点的内存快照。MAT 无法告诉您创建对象的原因,也无法显示已经被垃圾回收的对象。但是,如果将 MAT 与其他调试工具和技术一起使用,则通常可以很快克服内存泄漏。
要将应用程序配置为在引发 OutOfMemory 错误时生成堆转储,请添加以下 vm 参数:
-XX:+HeapDumpOnOutOfMemoryError
或者,您可以使用 jstack 从当前正在运行的 java 进程中获取 Heap dump。
jmap -dump:file=heap.bin
最后,您可以使用 MAT 的 Acquire Heap Dump 操作来选择本地计算机上的现有 Java 进程。
首次加载 Heap dump 时,MAT 将对其进行索引。这需要几分钟时间,但结果将保留下来,因此后续加载会很快。
2、了解直方图(Histogram)
当您第一次获取堆转储时,MAT 将向您显示应用程序内存使用情况的概述。
中间的饼图按保留大小显示最大的对象。这意味着,如果我们可以处理 java.lang.Thread 的特定实例,我们将节省 11.2Mb 和此应用程序中使用的 90% 以上的内存。虽然这看起来很有趣,但 java.lang.Thread 不太可能是这里的真正问题。为了更好地了解当前存在哪些对象,您可以使用 Histogram。
直方图显示特定类的实例数以及每个实例使用的内存量。当然,char[]、String 和 Object[] 不太可能是问题。为了帮助更好地组织此视图,您可以按 classloader 或 package 进行分组。这将使您能够专注于 Object。
也可以使用正则表达式筛选直方图。例如,我们可以只显示与模式 com.example.mat.* 匹配的类。
通过此视图,我们现在可以看到系统中有 100,000 个 Listener 对象处于活动状态。我们还可以看到每个 Object 使用的内存量。有两种计算方式:浅层堆(Shallow Heap)和保留堆( Retained Heap)。 浅堆是一个对象消耗的内存量。Object 需要 32 位(或 64 位,具体取决于架构)用于每个引用。整数和长整型等基元需要 4 或 8 个字节,等等。虽然这可能很有趣,但更有用的指标是 Retained Heap。
3、了解保留堆(Retained Heap)
保留堆显示在垃圾回收此对象时将删除的所有对象的浅堆大小之和。例如,如果一个 ArrayList 包含 100,000 个项目,每个项目需要 16 个字节,那么删除 ArrayList 将释放 16 x 100,000 + X,其中 X 是 ArrayList 的浅层大小。(注意:这假设这些对象仅被 ArrayList 引用,而不是被其他位置引用)。
保留堆的计算方法是将保留集中所有对象的大小相加。保留的 X 集是在收集 X 时 GC 将删除的对象集。
保留堆可以通过两种不同的方式计算,使用快速近似或精确保留大小。
通过计算 Retained Heap,我们现在可以看到 com.example.mat.Controller 持有大部分内存,即使它本身只有 24 字节。通过找到一种方法来释放 Controller,我们当然可以控制我们的内存问题。
4、支配者树(Dominator Tree)
了解保留堆的关键是查看支配树。支配树是由系统中的复杂对象图生成的树。dominator 树允许您识别最大的内存图。如果从 Root 到 Y 的每条路径都必须通过 X,则称对象 X 支配对象 Y。查看我们示例的支配树,我们可以开始看到大部分内存泄漏的位置。
通过查看支配树,我们可以很容易地看出问题不是 java.lang.Thread,而是保存内存的 Controller 和 Allocator。控制器将保留所有 100,000 个侦听器。通过删除释放这些对象或释放它们包含的列表,我们可能会改善我们的情况。支配树有一些有用的属性:
● 属于 X 的子树(在本例中为 com.example.mat.Controller)的所有对象都被称为 X 的保留集合中
● 如果 X 是 Y 的直接支配者(Controller 是分配器的直接支配),那么 X 的直接支配者(在我们的示例中是 java.lang.Thread)也支配 Y。
● 树中的父子关系不一定对应于 Object (对象) 图中的关系
从 Histogram 中,您还可以选择一个特定的类并找到主导该类实例的所有对象。
5、探索 GC 根的路径(GC Roots)
有时,您有大量 Object 集合,您确定要处理这些对象。找到 支配者 可能会有所帮助,但通常你需要从该 Object 到根的确切路径。例如,如果我现在正确处理了我的 Controller,那么我的内存问题肯定应该消失,不幸的是他们没有。
如果我现在选择 Listener 的一个实例,并查看 Paths To GC Roots,我可以看到 Controller 类(注意:类,而不是 Object)引用了 Listener 列表。这是因为其中一个列表被声明为 static。
您还可以浏览对 Object 的所有传入和传出引用。如果您想在 Object (对象) 图中查看对特定 Object 的所有引用,这将非常有用。
6、检查员(Inspector)
Inspector 提供有关当前所选 Class 或 Object 的详细信息。在此示例中,我们可以看到当前选定的 ArrayList 包含 100,000 个元素,并引用了位于内存位置 0x7f354ea68 的对象数组。
保持 Inspector 和 Snapshot 链接将为您提供有关所有选择的重要统计信息。
7、常见的内存反模式(Memory Anti-Patterns)
MAT 提供常见内存使用反模式的报告。这些可用于了解内存泄漏发生的位置,或者通过寻找一些可以清理以帮助提高性能的唾手可得的成果。
Heap Dump Overview 将向您显示有关 Heap Dump 的详细信息,并提供指向常用工具(如 Histogram)的链接。此外,还会显示正在运行的线程数、系统中的对象总数、堆的大小等信息。
Leak Suspects 报告显示可能的内存泄漏,并提供指向用于分析这些结果的工具和图表的链接。
另一个常见的反模式是使用大量的集合,每个集合中的条目都非常少。例如,如果我们的侦听器每个都有一组通知器(需要通知某些事件的项目),但这些通知器只是偶尔使用,我们最终会浪费大量空间。Java Collections 工具可以帮助解决这些问题。
通过运行 Collection -> Fill Ratio Report,我们可以看到有 100,000 个 ArrayLists 为空。如果以懒惰的方式分配这些(仅在需要时),我们将节省近 8Mb。
我们还可以使用 Collection Analysis 来查看数组填充率、集合大小统计数据和 map 冲突率。
8、Java 工具( Java Tools)
MAT 具有许多内置工具,用于生成针对 Java 运行时的详细信息量身定制的报告。例如,Threads and Stacks 报告将显示有关系统中所有 tread 的详细信息。您可以看到当前在每个堆栈上保持活动状态的局部变量。
您可以在系统中找到与特定模式匹配的所有字符串:
甚至在系统中找到字符数组中包含浪费空间的字符串(通常是由于重复使用子字符串)。
9、Object Query Language
正如我们所展示的,Eclipse Memory Analyzer 有很多工具可以帮助跟踪内存泄漏和过度内存使用。虽然大多数内存问题都可以使用上述技术来解决,但 Heap Dump 包含更多信息。对象查询语言 (OQL) 允许您根据堆转储的结果构建自己的报告。
OQL 是一种类似 SQL 的语言。只需将 Classes 视为表,将 Object 视为 rows,将 Field 视为列。例如。要显示 com.example.mat.Listener 类型的所有 Object,您只需编写:
select * from com.example.mat.Listener
可以使用不同的字段配置列,例如:
SELECT toString(l.message), l.message.count FROM com.example.mat.Listener
系统,其格式不是 “message:.*”
SELECT toString(s), s.count FROM java.lang.String s WHERE (toString(s) NOT LIKE “message.*”)
10、导出结果
内存分析器是创建有关应用程序内存状态的报告的出色工具。堆转储包含有关系统状态的宝贵信息,而 MAT 提供了访问此数据所需的工具。但是,就像许多打开的工具一样,如果缺少某些东西,您并没有被锁定,也不是运气不佳。使用 MAT,您可以将结果导出为多种不同的格式,包括 HTML、CSV 甚至纯文本。然后,您可以使用自己喜欢的电子表格程序(或您自己的工具)继续分析。
Eclipse Memory Analyzer 是一个强大的工具,所有 Java 开发人员都应该熟悉它。跟踪内存泄漏和其他与内存相关的问题通常具有挑战性,但希望使用 MAT 您可以相对较快地找到问题的根源。