JProfiler帮助文档

堆遍历器


堆快照

任何涉及对象之间引用的堆分析都需要一个堆快照,因为不可能询问JVM对一个对象的传入引用是什么 - 你必须通过迭代整个堆来回答这个问题。从该堆快照中,JProfiler创建了一个经过优化的内部数据库, 可以产生堆遍历器中的视图服务所需的数据。

堆快照有两个来源: JProfiler堆快照和HPROF/PHD堆快照。JProfiler堆快照支持堆遍历器的所有可用功能。 分析代理使用分析接口JVMTI迭代所有引用。如果被分析的JVM运行在一个不同的机器上,所有的信息都会传输到本地机器上, 并在那里进行进一步的计算。HPROF/PHD快照是通过JVM中的内置机制创建的,并以JProfiler可以读取的标准格式写入磁盘。 HPROF快照由HotSpot JVM提供,PHD快照由Eclipse OpenJ9 JVM产生。

在堆遍历器的概览页面上,你可以选择是创建JProfiler堆快照还是HPROF/PHD堆快照。默认情况下,建议使用JProfiler堆快照。 HPROF/PHD堆快照在特殊情况下很有用,将在其他章节中讨论。

选择步骤

堆遍历器由多个视图组成,这些视图显示了所选对象集的不同方面。在你生成堆快照后,你将看到堆上的所有对象。 每个视图都有导航操作,用于将一些选定对象变成当前对象集。 堆遍历器的头部区域显示当前对象集中包含多少对象的信息。

"类"视图是初始视图,它类似于实时内存部分中的"所有对象"视图。 通过选择一个类并调用使用→选定的实例,你将创建一个新的对象集, 该对象集只包含该类的实例。在堆遍历器中,"使用"总是意味着创建一个新的对象集。

对于新对象集,堆遍历器显示的类视图不会是你要关注的,因为它实际上只是将表过滤为之前选择的类。 相反,JProfiler建议使用"新对象集"对话框中的其他视图。你可以取消这个对话框以丢弃新对象集,并返回到之前的视图。 建议使用传出引用视图,但你也可以选择其他视图。这只是针对初始显示的视图,之后你可以在堆遍历器的视图选择器中切换视图。

头部区域现在告诉你有两个选择步骤,并包括计算保留和深层大小的链接,或者使用当前对象集保留的所有对象。 后者将增加另一个选择步骤,并建议使用类视图,因为该对象集中可能会有多个类。

在堆遍历器的下半部分,列出了到目前为止的所有选择步骤。点击这些超链接将带你返回到任意一个选择步骤。 返回到第一个数据集也可以通过使用工具栏中的转到开始按钮。如果你需要回溯分析, 工具栏中的后退和前进按钮很有用。

类视图

堆遍历器顶部的视图选择器包含五个视图,显示当前对象集的不同信息。其中第一个是"类"视图。

类视图类似于实时内存部分的"所有对象"视图,有一个聚合级别选择器,可以将类以包分组。 此外,它还可以显示类的估计保留大小。这是一个类的所有实例被从堆中移除后所释放的内存量。 如果你点击计算估计的保留大小超链接,将添加一个保留大小列。 显示的保留大小是估计的下限,计算精确值会太慢。如果你确实需要一个精确值,选择感兴趣的类或包,然后使用 新对象集头部区域的计算保留和深层大小超链接。

基于你选择的一个或多个类或包,你可以选择他们自己的实例、相关的java.lang.Class 对象 或所有保留对象。双击是使用所选实例最快捷的选择模式。如果有多种选择模式可供选择,就像本例中一样, 一个使用下拉菜单显示在视图上方。

在解决与类加载器相关的问题时,你通常要按类加载器对实例进行分组。 检查选项卡提供了一个"按类加载器分组"的检查,这个检查在类视图上是可用的,因为它在这种情况下特别重要。 如果执行该分析,顶部的分组表会显示所有类加载器。选择一个类加载器会在下面的视图中相应地过滤数据。 当你切换到堆遍历器的其他视图时,该分组表仍会保持原样,直到你执行另一个选择步骤。 然后,该类加载器的选择会变成选择步骤的一部分。

分配记录视图

当缩小内存泄漏的可疑范围或试图减少内存消耗时,对象分配的信息可能很重要。对于JProfiler堆快照, "分配"视图显示了已被记录分配的对象的分配调用树和分配热点。其他对象被归入分配调用树中的"未记录的对象"节点。 对于HPROF/PHD快照,此视图不可用。

就像在类视图中一样,你可以选择多个节点并使用顶部使用选定的按钮来创建一个新的选择步骤。 在"分配热点"视图模式下,你还可以选择回溯跟踪中的节点。这将只选择关联的顶层热点中的对象, 这些对象在以所选回溯跟踪结束的调用堆栈上被分配。

JProfiler在记录分配时可以保存的另一个信息是对象被分配的时间。 堆遍历器中的"时间"视图显示了当前对象集中所有记录实例的分配时间的直方图。 你可以单击并拖动以选择一个或多个区间,然后通过使用选定的按钮创建一个新的对象集。

为了更精确地选择时间间隔,你可以指定一个书签范围。 在第一个和最后一个选定书签之间的所有对象将被标记。

除了时间视图外,分配时间在引用视图中以单独一列显示。但是,默认情况下,分配时间记录不被启用。 你可以直接在时间视图中打开它,或编辑会话设置对话框中的高级设置 -> 内存分析设置。

最大对象视图

最大对象视图显示了当前对象集中最重要的对象列表。这里的"最大"指的是,如果从堆中删除这些对象,会释放最多的内存。 这里的大小叫做保留大小。相反,深层大小是指所有通过强引用可以触达的对象的总大小。

每一个对象都可以被展开,以显示到被这个对象保留的其他对象的传出引用。通过这种方式,你可以递归地展开如果一个祖先被 删除将被垃圾回收的保留对象树。这种树被称为"支配树"。这棵树中每个对象显示的信息与传出引用视图类似,只是只显示支配引用。

并非所有被支配的对象都是由其支配者直接引用的。例如,考虑下图中的引用:

GC 根直接支配直接支配间接支配对象 A对象 B2对象 B1对象 C

对象A支配对象B1和B2,它对对象C没有直接引用,B1和B2都引用C,B1和B2都不支配C,但A支配了C。 在这种情况下,B1、B2和C在支配树中被列为A的直接子级,而C不会被列为B1和B2的子级。 对于B1和B2,会显示它们在A中被持有的字段名。对于C,在引用节点上显示"[传递引用]"。

在支配树中每个引用节点的左侧,一个尺寸栏显示了目标对象仍然保留的顶层对象的大小的百分比。 当你进一步深入到树中时,数字会减小。在视图设置中,你可以将百分比基数改为总堆大小。

支配树有一个内置的截止点,会排除所有保留大小低于父对象保留大小0.5%的对象。这是为了避免过长的小支配对象列表, 以免分散对重要对象的注意力。如果发生这样的截止,将显示一个特殊的"截止"子节点,通知你该层级未显示的对象数量、 它们的总保留大小和单个对象的最大保留大小。

代替显示单个对象,支配树还可以将最大对象按类分组。视图顶部的分组下拉框包含一个复选框,可以激活这种显示模式。 此外,你还可以在顶层添加一个类加载器分组。类加载器分组会在计算出最大对象后应用,并显示谁加载了最大对象的类。 如果你想分析某一个特定的类加载器的最大对象,可以先使用"按类加载器分组"检查。

最大对象视图上方的视图模式选择器允许你切换到旭日图。该图由一系列同心分段圆环组成,在一张图里显示 支配树,直到最大深度的全部内容。引用从最内部的圆环开始,向环的外缘传播。这种可视化给你一个扁平话 的视角,很高的信息密度可以让你发现引用模式,并通过特殊的颜色编码一目了然地看到原始和对象数组。

如果当前对象集是整个堆,那么圆的周长对应于使用的堆大小。因为最大对象视图只显示 保留的超过总堆0.1%的对象,这意味着相当大的扇形是空的,这些空的扇形对应于 所有那些没有被这些最大对象保留的对象。

点击任何一个圆环分段,会为圆环设置一个新的根,从而扩展你能够在图中看到的最大深度。 点击图中的空心中心,会恢复之前的根。如果设置了一个新根,圆环的总周长对应,该根对象的保留大小。 一个空的扇形代表根对象的自身大小和不在最大保留对象列表中的其他对象。如果当前对象不是整个堆, 圆的总周长对应于所有显示的最大对象的总和,没有空扇区显示。

当你将鼠标悬停在上面,更多关于实例和它们立即保留的对象信息会显示在图的右侧。 当鼠标在所有圆环分段外面时,右侧的列表会显示最内部圆环的最大对象。 将鼠标悬停在该列表上会高亮显示对应的圆环分段,点击列表条目会为该图设置一个新根。 要创建一个新对象集,你可以从上下文菜单中选择操作,既可以在圆环分段上也可以在列表条目上。

引用视图

与前边的视图不同,引用视图只有在你至少执行了一个选择步骤时才可用。对于初始对象集来说,这些视图并没有用处, 因为传入和传出引用视图显示的是所有单个对象,而合并的引用视图只对于关注的对象集才有可解释性。

该传出引用视图类似于IDE中调试器显示的视图。打开一个对象时,可以看到原始数据和对其他对象的引用。 可以选择任何引用类型作为一个新的对象集,并且可以同时选择多个对象。像在类视图中一样, 你可以选择保留对象或相关的java.lang.Class 对象。如果所选对象是一个标准集合, 你也可以通过一个操作选择所有包含的元素。对于类加载器对象,有一个选项可以选择所有加载的实例。

默认情况下不显示带有空引用的字段,因为这些信息可能会分散内存分析的注意力。 如果出于调试目的你想看到所有字段,你可以在视图设置中改变这种行为。

除了对显示的实例进行简单的选择外,传出引用视图还具有 强大的过滤功能。 对于实时会话,传出引用视图和传入引用视图都具有高级操作和显示功能,这些功能将在同一章节中讨论。

传入引用视图是解决内存泄漏的主要工具。要找出一个对象没有被垃圾回收的原因, 可以使用显示到GC根的路径按钮查找到垃圾回收器根的引用链。 内存泄漏一章有关于这一重要主题的详细信息。

合并的引用

检查很多不同对象的引用可能会很冗长乏味,所以JProfiler可以向你展示当前对象集中所有对象合并的传出和传入引用。 默认情况下,引用是按类聚合的。如果一个类的实例被同一类的其他实例引用, 则会插入一个 特殊节点, 显示原始实例加上这些类递归引用的实例。这种机制会自动折叠常见数据结构中的内部引用链,例如在一个链表中。

你也可以选择按字段分组显示合并的引用。在这种情况下,每个节点都是一个引用类型,例如一个类的特定字段或一个数组的内容。 对于标准集合,会打破被压缩的累积内部引用链,所以你看到的引用类型例如"java.lang.HashMap的映射值"。 与类聚合不同,这种机制只对JRE标准库中明确支持的集合有效。

在"合并的传出引用"视图中,实例计数指的是引用的对象。在"合并的传入引用"视图中,每一行都可以看到两个实例计数。 第一个实例计数显示了当前对象集中有多少个实例沿着这个路径被引用。节点左侧的条形图标将这个分数可视化。 箭头图标后的第二个实例数指的是持有对父节点的引用的对象。在执行选择步骤时, 你可以选择是否要从当前对象集中选择以所选方式引用的对象,还是对具有所选引用的对象--引用持有者感兴趣。

通过"合并的支配引用"视图,你可以找出哪些引用必须被删除,以便当前对象集中的部分或全部对象可以被垃圾回收。 支配引用树可以被解释为最大对象视图中支配树的合并倒置,以类聚合。引用箭头可能并不代表两个类之间的直接引用, 中间可能有其他类持有非支配引用。在多个垃圾回收器根的情况下,当前对象集中的部分或全部对象可能不存在支配引用。

默认情况下,"合并的支配引用"视图显示传入支配引用,通过打开树,可以到达GC根所持有的对象。有时, 引用树可能会沿着许多不同的路径通向同一个根对象。通过在视图顶部的下拉框中选择"GC根到对象"的视图模式, 可以看到反转视角,即根在顶层,而当前对象集的对象在叶节点。在这种情况下,引用从顶层向叶节点走。 哪种视角更好,取决于你要排除的引用是靠近当前对象集还是靠近GC根。

检查

"检查"视图本身并不显示数据。它提供了一些堆分析,这些分析根据其他视图中没有的规则创建新的对象集。 例如,你可能想查看被ThreadLocal保留的所有对象,这在引用视图中是无法做到的。 检查被分为几个类别,并在其描述中进行了解释。

检查可以将计算出的对象集分割成组。组显示在堆遍历器顶部的表格中。例如,"重复的字符串"检查将重复的字符串值分组显示。 如果你在引用视图中,那么你可以在下面看到带有选定字符串值的java.lang.String 实例。 初始,分组表中的第一行被选中。通过改变选择,就会改变当前的对象集。实例计数大小 列告诉你选择一行时,当前对象集有多大。

组选择在堆遍历器中不是一个单独的选择步骤,但它成为检查所做选择步骤的一部分。你可以在底部的选择步骤窗格中看到组选择, 当你更改组选择时,选择步骤窗格会立即更新。

每一个创建组的检查都会决定哪些组在检查中是最重要的。因为这并不总是与其他列中的一个自然排序相对应, 所以分组表中的优先级列包含一个数值,该数值强制检查的排序。

对于大型堆来说,检查的计算成本很高,所以结果会被缓存起来。这样一来,你可以回溯历史,查看之前计算的检查结果,无需等待。

堆遍历器图

最逼真展现实例及其引用的方式是图。虽然图的视觉密度较低,对某些类型的分析不现实, 但它仍然是可视化对象之间关系的最佳方式。例如,循环引用在树中很难解释,但在图中却一目了然。 此外,将传入引用和传出引用放在一起查看可能会有用处,而在树形结构中是不可能做到这一点,你只能看到其中一个或另一个。

堆遍历器图不会自动显示当前对象集的任何对象,当你更改当前对象集时也不会清除。 你可以通过从传出引用视图、传入引用视图或最大对象视图中手动选择一个或多个实例,并使用在图表中显示操作 将选定的对象添加到图中。

图中的包名默认是缩短的。与CPU调用图一样,你可以在视图设置中启用完整显示。引用被涂成箭头。如果将鼠标移动到引用上, 将显示一个工具提示窗口,显示指定引用的详细信息。从引用视图中手动添加的实例有蓝色背景。越是最近添加的实例,背景颜色越深。 垃圾回收器根的背景为红色,类的背景为黄色。

默认情况下,引用图只显示当前实例的直接传入和传出引用,你可以通过双击任何对象来展开图。 将根据你移动的方向,展开该对象的直接传入或直接传出引用。通过实例左右两边的展开控件, 你可以有选择地打开传入和传出引用。如果你需要回溯,可以使用撤销功能恢复图的以前的状态,这样你就不会因为节点太多而分心。 要切除图,有删除所有未连接的节点,甚至删除所有对象的操作。

像在传入引用视图中一样,该图中有一个 显示到GC根的路径按钮,如果有的话, 它将展开一个或多个到垃圾回收器根的引用链。 此外,还有一个 查找两个选定节点之间的路径操作,如果选择了两个实例,该操作将被激活。 如果找到合适的路径,它会以红色显示。

初始对象集

当你生成堆快照时,可以指定控制初始对象集的选项。如果你已经记录了分配, 则选择记录的对象复选框将初始显示的对象集限制为这些已记录的对象。 这些数字通常会与实时内存视图中的数字不同,因为未引用的对象会被堆遍历器移除。 未记录的对象仍然存在于堆快照中,只是不显示在初始对象集中。通过进一步的选择步骤,你可以触达未记录的对象。

此外,堆遍历器会执行垃圾回收,并删除弱引用对象,软引用除外。这通常是你想要的,因为在寻找内存泄漏时, 弱引用对象会分散注意力,而只有强引用对象才是相关的。然而,如果有时你对弱引用对象感兴趣,你也可以告诉堆遍历器保留它们。 JVM中的四种弱引用类型是"软引用"、"弱引用"、"虚引用"和"finalizer引用", 你可以选择其中的哪一种类型的对象应该被保留在堆快照中。

如果存在,可以通过使用堆遍历器中的"弱引用"检查,从当前对象集中选择或删除弱引用对象。

标记堆

通常你想查看已经为某个用例分配的对象。虽然你可以通过围绕该用例开始和停止分配记录来实现这一目的, 但有一种更好的方法,它可以减少很多开销,并为其他目的保留分配记录功能: 堆遍历器顶部菜单的标记堆操作,在分析菜单中也有显示, 或作为一个触发操作,将堆上的所有对象标记为"旧"。当你生成下一个堆快照时,就很清楚"新"对象是哪些。

如果之前有一个堆快照或标记堆调用,则堆遍历器的标题区域会显示新实例数和两个标题是使用新的使用旧的链接, 这两个链接允许你选择那个时间点之后分配的对象或之前分配的但依然存活的对象。 这些信息对每个对象集来说都是有效的,所以你可以先深入了解,然后再选择新的或旧的实例。