JProfiler帮助文档

Heap Walker

堆快照 (Heap snapshots)

涉及对象间引用的任何堆分析都需要堆快照 (Heap snapshot),因为无法直接向 JVM 查询某个对象的入站引用 (Incoming references)。必须遍历整个堆才能回答这个问题。JProfiler 会根据该堆快照创建一个内部数据库,该数据库经过优化,用于生成为 Heap Walker 中的视图 (View) 提供服务所需的数据。

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

在 Heap Walker 的概览页面上,您可以选择创建 JProfiler 堆快照还是 HPROF/PHD 堆快照。默认情况下,推荐使用 JProfiler 堆快照。HPROF/PHD 堆快照在特定情况下非常有用,这将在另一章节中进行讨论。

选择步骤 (Selection steps)

Heap Walker 由多个视图 (View) 组成,展示所选对象集的各个方面。获取堆快照后,您立即看到的是堆上的所有对象。每个视图都有导航操作,可将某些选定对象转换为当前对象集 (Current object set)。Heap Walker 的标题区域会显示当前对象集包含多少个对象的信息。

最初,您看到的是“类 (Classes)”视图,它类似于实时内存部分中的“所有对象 (All objects)”视图。通过选择一个类并调用 使用 (Use)→选定实例 (Selected Instances),您可以创建一个仅包含该类实例的新对象集 (Object set)。在 Heap Walker 中,“使用”始终意味着创建一个新的对象集。

对于新的对象集 (Object set),显示 Heap Walker 的类视图并没有太大意义,因为这实际上只是将表格过滤到之前选定的类。相反,JProfiler 会通过“新对象集 (New object set)”对话框推荐另一个视图 (View)。您可以取消此对话框以放弃新对象集并返回上一个视图。系统推荐的是出站引用 (Outgoing references) 视图,但您也可以选择其他视图。这仅适用于最初显示的视图,之后您可以在 Heap Walker 的视图选择器中切换视图。

此时标题区域会提示有两个选择步骤,并包含用于计算保留大小 (Retained size) 和深层大小 (Deep size) 的链接,或者用于使用当前对象集 (Object set) 保留的所有对象的链接。后者将添加另一个选择步骤,并推荐类视图 (View),因为该对象集中可能包含多个类。

在 Heap Walker 的下半部分,列出了截至当前的所有选择步骤。点击超链接可带您返回任意选择步骤。也可以通过工具栏中的 转到起始位置 (Go To Start) 按钮访问第一个数据集。如果您在分析中需要回溯,工具栏中的前进和后退按钮会非常有用。

类视图 (Classes view)

Heap Walker 顶部的视图 (View) 选择器包含五个视图,展示当前对象集 (Object set) 的不同信息。其中第一个是“类 (Classes)”视图。

类视图类似于实时内存部分中的“所有对象 (All objects)”视图,并且具有一个可将类分组到包中的聚合级别 (Aggregation level) 选择器。此外,它还可以显示类的估计保留大小 (Retained size)。这是从堆中移除某个类的所有实例时将释放的内存量。如果点击 计算估计保留大小 (Calculate estimated retained sizes) 超链接,则会添加一个新的 保留大小 (Retained Size) 列。显示的保留大小是估计的下限值,计算精确数值会过慢。如果您确实需要精确数值,请选择感兴趣的类或包,并使用新对象集 (Object set) 标题中的 计算保留大小和深层大小 (Calculate retained and deep sizes) 超链接。

根据您选择的一个或多个类或包,您可以选择实例本身、关联的 java.lang.Class 对象或所有保留的对象。双击是最快捷的选择模式,将使用选定的实例。如果存在多种可用选择模式(如本例所示),视图 (View) 上方会显示一个 使用 (Use) 下拉菜单。

在解决与类加载器 (Classloader) 相关的问题时,通常需要按实例的类加载器对其进行分组。检查 (Inspections) 选项卡提供了“按类加载器分组 (Group by class loaders)”检查,该检查在类视图 (View) 中可用,因为在该上下文中尤其重要。如果执行该分析,顶部的分组表格将显示所有类加载器。选择一个类加载器会在下方视图中对数据进行相应的过滤。当您切换到 Heap Walker 的其他视图时,该分组表格仍会保留,直到您执行另一个选择步骤为止。届时,所选的类加载器将成为该选择步骤的一部分。

分配记录视图 (Allocation recording views)

在缩小内存泄漏排查范围或尝试减少内存消耗时,对象在何处被分配 (Allocated) 的信息可能非常重要。对于 JProfiler 堆快照,“分配 (Allocations)”视图 (View) 显示已记录分配的对象的分​​配调用树 (Allocation call tree) 和分配热点 (Allocation hot spot)。其他对象则分组在分配调用树中的“未记录对象 (unrecorded objects)”节点中。对于 HPROF/PHD 快照,此视图不可用。

与类视图 (View) 中一样,您可以选择多个节点,并使用顶部的 使用选定项 (Use Selected) 按钮创建新的选择步骤。在“分配热点 (Allocation hot spots)”视图模式下,您还可以选择回溯 (Backtrace) 中的节点。这将仅选择关联顶级热点 (Hot spot) 中在以所选回溯结尾的调用栈 (Call stack) 上分配 (Allocated) 的对象。

JProfiler 在记录分配 (Allocation) 时可以保存的另一项信息是对象被分配的时间。Heap Walker 中的“时间 (Time)”视图 (View) 显示当前对象集 (Object set) 中所有已记录实例的分配时间直方图。您可以点击并拖动以选择一个或多个时间间隔,然后使用 使用选定项 (Use Selected) 按钮创建新的对象集。

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

除了时间视图 (View) 之外,分配 (Allocation) 时间还作为单独的列显示在引用视图中。但是,分配时间记录默认未启用。您可以直接在时间视图中将其开启,或者在会话 (Session) 设置对话框中的 高级设置 (Advanced Settings) -> 内存分析 (Memory Profiling) 中编辑该设置。

最大对象视图 (Biggest objects view)

最大对象视图 (View) 显示当前对象集 (Object set) 中最重要的对象列表。在本文中,“最大”是指如果从堆中移除这些对象,将释放最多内存的对象。该大小称为保留大小 (Retained size)。相比之下,深层大小 (Deep size) 是通过强引用可达的所有对象的总大小。

可以展开每个对象以显示该对象保留的其他对象的出站引用。通过这种方式,您可以递归展开保留对象的树,如果其中一个祖先对象被移除,这些对象将被垃圾回收。这种树称为“支配树 (Dominator tree)”。在此树中为每个对象显示的信息类似于出站引用视图 (View),不同之处在于仅显示支配引用。

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

GC根直接支配直接支配间接支配Object AObject B2Object B1Object C

对象 A 支配对象 B1 和 B2,并且它对对象 C 没有直接引用。B1 和 B2 都引用了 C。B1 和 B2 都不支配 C,但 A 支配 C。在这种情况下,B1、B2 和 C 在支配树中作为 A 的直接子节点列出,而 C 不会列为 B1 和 B2 的子节点。对于 B1 和 B2,会显示 A 中持有它们所对应的字段名称。对于 C,在引用节点上会显示“[传递引用 (transitive reference)]”。

在支配树中每个引用节点的左侧,大小柱状条显示目标对象仍保留顶级对象保留大小 (Retained size) 的百分比。当您在树中进一步向下钻取时,这些数值会递减。在视图 (View) 设置中,您可以将百分比基准更改为总堆大小。

支配树具有内置的截断阈值,会剔除保留大小 (Retained size) 低于父对象保留大小 0.5% 的所有对象。即使低于截断阈值,也始终至少显示 20 个对象。这是为了避免过长的小型被支配对象列表分散对重要对象的注意力。如果发生此类截断,将显示一个特殊的“截断 (Cutoff)”子节点,通知您该层级上未显示的对象数量、它们的总保留大小以及单个对象的最大保留大小。

支配树除了显示单个对象外,还可以将最大对象按类进行分组。视图 (View) 顶部的分组下拉列表中包含一个复选框,可激活此显示模式。此外,您可以在顶级添加类加载器 (Classloader) 分组。类加载器分组在计算出最大对象之后应用,并显示是由谁加载了这些最大对象的类。如果您希望分析某个特定类加载器的最大对象,可以先使用“按类加载器分组 (Group by class loaders)”检查。

分组下拉列表包含一个 按容器分组 (Group by container) 复选框,可按拥有最大对象的框架容器 (Container) 对其进行分组。顶级仅显示在当前对象集 (Object set) 中检测到的容器组节点,例如 Spring 应用上下文 (Spring application context) 或 HTTP 会话 (HTTP session),并显示该容器管理的对象的累计保留大小 (Retained size)。如果未检测到受支持的容器,视图 (View) 将为空。容器可以嵌套或管理同一对象,因此它们的保留大小描述的是各自占用的空间,并不一定具有可加性。

展开容器 (Container) 节点时,可以直接看到受管对象,例如 Spring Bean 或会话 (Session) 属性,每个对象都有自己的保留大小 (Retained size)。展开受管对象会显示其下方的常规支配树。

最大对象视图 (View) 上方的视图模式选择器允许您切换到旭日图 (Sunburst diagram)。该图表由一系列同心分段环组成,在单张图像中显示支配树直至最大深度的全部内容。引用始于最内环,并向圆环外缘传播。这种可视化为您提供了一个扁平化且信息密度高的视角,使您能够发现引用模式,并通过特殊的颜色编码一眼识别出大型基本类型数组和对象数组。

如果当前对象集 (Object set) 是整个堆且未激活容器 (Container) 分组,则圆的总周长对应于已用堆大小。由于最大对象视图 (View) 仅显示保留超过总堆 0.1% 的对象,这意味着将有一个相当大的扇区为空,对应于未被这些最大对象保留的所有对象。启用容器分组后,总周长则对应于所有显示的容器占用空间的总和。由于容器可能重叠,该总和可能会超过已用堆大小。在此模式下,不会为所显示容器之外的对象显示空扇区。

点击任意环形分段可为圆设置新的根节点,从而扩展您在图表中可以看到的最大深度。点击图表的中心空白区域可恢复上一个根节点。如果设置了新的根节点,圆的总周长对应于根对象的保留大小 (Retained size)。对于容器 (Container) 根节点,它对应于其受管对象的累计保留大小。空扇区表示常规根对象的自身大小 (Self-size) 以及未出现在最大保留对象列表中的其他对象。如果当前对象集 (Object set) 不是整个堆且未设置新的根节点,则总周长对应于所有显示的最大对象之和,并且不显示空扇区。容器根节点也没有空扇区,因为其大小是其显示的受管对象之和。

将鼠标悬停在实例及其直接保留的对象上时,图表右侧会显示有关它们的更多信息。对于容器 (Container),侧边面板则显示其受管对象。当鼠标位于任何环形分段之外时,右侧列表会显示最内环中的最大对象。将鼠标悬停在该列表上会高亮显示对应的环形分段,点击列表项可为图表设置新的根节点。要创建新的对象集 (Object set),您可以从环形分段以及列表项上的上下文菜单操作中进行选择。

引用视图 (Reference views)

与前面的视图 (View) 不同,引用视图仅在您执行了至少一个选择步骤后才可用。对于初始对象集 (Object set),这些视图没有用处,因为入站和出站引用视图显示所有单独的对象,而合并引用视图只能针对聚焦的对象集进行解释。

出站引用视图类似于 IDE 中调试器所显示的视图 (View)。打开某个对象时,您可以看到基本类型数据以及对其他对象的引用。任何引用类型都可以被选为新的对象集 (Object set),并且您可以一次选择多个对象。与类视图中一样,您可以选择保留的对象或关联的 java.lang.Class 对象。如果选定对象是标准集合,您还可以通过单个操作选择包含的所有元素。对于类加载器 (Classloader) 对象,提供了选择所有已加载实例的选项。

默认情况下不显示带有 null 引用的字段,因为此类信息可能会干扰内存分析。如果出于调试目的希望查看所有字段,可以在视图 (View) 设置中更改此行为。

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

入站引用视图是解决内存泄漏的主要工具。要找出对象未被垃圾回收的原因,显示到 GC 根的路径 (Show Paths To GC Root) 按钮将查找指向垃圾回收器根节点 (GC Root) 的引用链。有关这一重要主题的详细信息,请参阅关于内存泄漏的章节。

合并引用 (Merged references)

检查许多不同对象的引用可能会很繁琐,因此 JProfiler 可以向您显示当前对象集 (Object set) 中所有对象的合并出站和入站引用。默认情况下,引用按类进行聚合。如果某个类的实例被同一类的其他实例引用,则会插入一个  特殊节点,用于显示原始实例以及来自这些类递归引用的实例。该机制会自动折叠常见数据结构(如链表)中的内部引用链。

您也可以选择显示按字段分组的合并引用。在这种情况下,每个节点都是一种引用类型,例如类的特定字段或数组的内容。对于标准集合,会破坏累加的内部引用链会被紧凑压缩,因此您会看到类似“java.util.HashMap 的 Map 值”之类的引用类型。与类聚合不同,该机制仅适用于 JRE 标准库 (Library) 中明确支持的集合。

在“合并出站引用 (Merged outgoing references)”视图 (View) 中,实例计数指的是被引用的对象。在“合并入站引用 (Merged incoming references)”视图中,您在每行会看到两个实例计数。第一个实例计数显示当前对象集 (Object set) 中有多少实例沿着此路径被引用。节点左侧的柱状图标直观地显示了该比例。箭头图标后面的第二个实例计数指的是持有对父节点引用的对象。在执行选择步骤时,您可以选择是要从当前对象集中选择按所选方式引用的对象,还是对具有所选引用的对象(即引用持有者)感兴趣。

通过“合并支配引用 (Merged dominating references)”视图 (View),您可以找出必须移除哪些引用才能使当前对象集 (Object set) 中的部分或全部对象被垃圾回收。支配引用树可以理解为最大对象视图中支配树的合并反向树(按类聚合)。引用箭头可能并不表示这两个类之间的直接引用,中间可能存在持有非支配引用的其他类。在存在多个垃圾回收器根节点 (GC Roots) 的情况下,当前对象集中的部分或全部对象可能不存在支配引用。

默认情况下,“合并支配引用 (Merged dominating references)”视图 (View) 显示入站支配引用,通过展开树,您可以访问 GC 根持有的对象。有时,引用树可能会沿着许多不同的路径通向相同的根对象。通过在视图顶部的下拉列表中选择“GC 根到对象 (GC roots to objects)”视图模式,您可以看到反向视角,其中根位于顶层,而当前对象集 (Object set) 中的对象位于叶节点中。在这种情况下,引用从顶级指向叶节点。哪种视角更好取决于您要消除的引用是靠近当前对象集还是靠近 GC 根。

检查 (Inspections)

“检查 (Inspections)”视图 (View) 本身不显示数据。它提供了许多堆分析功能,可以根据其他视图中不可用的规则创建新的对象集 (Object set)。例如,您可能希望查看由 ThreadLocal 保留的所有对象。这在引用视图中是无法做到的。检查分为几个类别,并在各自的说明中进行了解释。

检查可以将计算出的对象集 (Object set) 划分为若干组。组显示在 Heap Walker 顶部的表格中。例如,“重复字符串 (Duplicate strings)”检查将重复的字符串值显示为组。如果您处于引用视图 (View) 中,则可以在下方看到具有所选字符串值的 java.lang.String 实例。最初,选中组表格中的第一行。通过更改选择,您可以更改当前对象集。组表格的 实例计数 (Instance Count) 和 大小 (Size) 列会告诉您在选择某一行时当前对象集的大小。

组选择在 Heap Walker 中并不是独立的选择步骤,而是成为该检查所执行的选择步骤的一部分。您可以在底部的选择步骤窗格中看到组选择。当您更改组选择时,选择步骤窗格会立即更新。

创建组的每个检查都会确定在该检查上下文中哪些组最重要。由于这并不总是与其中某个其他列的自然排序顺序相对应,因此组表格中的 优先级 (Priority) 列包含一个数值,用于强制执行该检查的排序顺序。

对于大型堆,计算检查的开销可能很大,因此结果会被缓存。这样,您就可以在历史记录中返回并查看先前计算的检查结果,而无需等待。

Heap Walker 图形 (Heap walker graph)

实例连同其引用的最真实表示是图形 (Graph)。虽然图形的视觉密度较低,且对于某些类型的分析不切实际,但它仍然是可视化对象之间关系的最佳方式。例如,循环引用在树中很难解释,但在图形中一目了然。此外,同时查看入站和出站引用可能很有帮助,这在只能查看其中一种引用的树形结构中是不可能的。

Heap Walker 图形 (Graph) 不会自动显示当前对象集 (Object set) 中的任何对象,在您更改当前对象集时也不会被清除。您可以从出站引用视图 (View)、入站引用视图或最大对象视图中选择一个或多个实例,并使用 在图形中显示 (Show In Graph) 操作手动将所选对象添加到图形中。

图形 (Graph) 中的包名默认会缩短。与 CPU 调用图 (Call graph) 中一样,您可以在视图 (View) 设置中启用完整显示。引用绘制为箭头。如果将鼠标悬停在引用上,将显示一个工具提示窗口,展示该特定引用的详细信息。从引用视图手动添加的实例具有蓝色背景。实例添加得越晚,背景颜色越深。垃圾回收器根节点 (GC Roots) 具有红色背景,类具有黄色背景。

默认情况下,引用图形 (Graph) 仅显示当前实例的直接入站和出站引用。您可以通过双击任何对象来展开图形。这将展开该对象的直接入站引用或直接出站引用,具体取决于您移动的方向。利用实例左右两侧的展开控件,您可以有选择地打开入站和出站引用。如果您需要回溯,请使用撤销功能恢复图形的先前状态,以免被过多节点分散注意力。要修剪图形,提供了用于移除所有未连接节点乃至移除所有对象的操作。

与入站引用视图 (View) 中一样,图形 (Graph) 中有一个 显示到 GC 根的路径 (Show Paths To GC Root) 按钮,如果存在,它将展开一条或多条指向垃圾回收器根节点的引用链。此外,还有一个 查找两个选定节点之间的路径 (Find Path Between Two Selected Nodes) 操作,在选中两个实例时激活。它可以搜索有向路径和无向路径,并且还可以选择沿着弱引用搜索。如果找到合适的路径,将以红色显示。

初始对象集 (Initial object set)

获取堆快照时,您可以指定用于控制初始对象集 (Object set) 的选项。如果您记录了分配 (Allocation),选择已记录对象 (Select recorded objects) 复选框会将最初显示的对象限制为已记录的对象。这些数量通常与实时内存视图 (View) 中的数量不同,因为无引用对象已被 Heap Walker 移除。未记录的对象仍然存在于堆快照中,只是未在初始对象集中显示。通过进一步的选择步骤,您可以访问未记录的对象。

此外,Heap Walker 会执行垃圾回收并移除弱引用对象(软引用除外)。这通常是符合期望的,因为在查找仅与强引用对象相关的内存泄漏时,弱引用对象会产生干扰。但是,如果您对弱引用对象感兴趣,可以指示 Heap Walker 保留它们。JVM 中的四种弱引用类型为“软引用 (soft)”、“弱引用 (weak)”、“虚引用 (phantom)”和“终结器引用 (finalizer)”,您可以选择其中哪一种足以在堆快照中保留对象。

如果存在弱引用对象,可以通过使用 Heap Walker 中的“弱引用 (Weak reference)”检查,将其从当前对象集 (Object set) 中选出或移除。

标记堆 (Marking the heap)

通常,您希望查看针对特定用例分配 (Allocated) 的对象。虽然您可以通过在该用例前后启动和停止分配记录来实现此目的,但有一种开销小得多且能将分配记录功能保留用于其他目的的更好方法:在 Heap Walker 概览中宣传的并在 分析 (Profiling) 菜单中或作为触发器 (Trigger) 操作提供的 标记堆 (Mark Heap) 操作,可将堆上的所有对象标记为“旧”。当您获取下一个堆快照时,就可以明确哪些是“新”对象。

如果之前存在堆快照或调用过标记堆,Heap Walker 的标题区域会显示新的实例计数以及名为 使用新对象 (Use new) 和 使用旧对象 (Use old) 的两个链接,允许您选择自该时间点以来分配 (Allocated) 的实例,或选择之前分配的存活实例。此信息对每个对象集 (Object set) 都可用,因此您可以先向下钻取,稍后再选择新实例或旧实例。