JProfiler帮助文档

堆遍历器 (Heap Walker)

堆快照 (Heap Snapshots)

任何涉及对象间引用的堆分析都需要一个堆快照,因为无法直接向JVM询问某个对象的所有入引用。必须遍历整个堆才能回答这个问题。JProfiler会基于该堆快照创建一个内部数据库,该数据库针对堆遍历器 (Heap Walker) 中各视图的数据需求进行了优化。

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

在堆遍历器的概览页面,你可以选择创建JProfiler堆快照或HPROF/PHD堆快照。默认推荐使用JProfiler堆快照。HPROF/PHD堆快照适用于某些特殊场景,相关内容请参见另一章节

选择步骤 (Selection Steps)

堆遍历器由多个视图 (view) 组成,展示所选对象集的不同方面。获取堆快照后,首先看到的是堆中的所有对象。每个视图都提供导航操作,可将部分选中对象转为当前对象集 (current object set)。堆遍历器的头部区域会显示当前对象集包含的对象数量等信息。

初始时,你会看到“类 (Classes)”视图,这与实时内存部分中的“所有对象 (All objects)”视图类似。选择一个类并执行Use→Selected Instances后,会创建一个只包含该类实例的新对象集。在堆遍历器中,“使用 (using)”始终意味着创建一个新对象集。

对于新对象集,继续显示类视图意义不大,因为这实际上只是对表格进行了过滤。JProfiler会通过“新对象集 (New object set)”对话框建议切换到其他视图。你可以取消该对话框以放弃新对象集并返回上一个视图。默认建议切换到“出引用 (outgoing references)”视图,但你也可以选择其他视图。这只是初始显示,之后可以在堆遍历器的视图选择器中随时切换视图。

头部区域现在会显示有两个选择步骤,并包含用于计算保留 (retained) 和深度 (deep) 大小的链接,或用于选中当前对象集所保留的所有对象。后者会增加一个选择步骤,并建议切换到类视图,因为该对象集很可能包含多个类。

在堆遍历器下方,会列出到当前为止的所有选择步骤。点击超链接可返回任意选择步骤。第一个数据集也可以通过工具栏中的Go To Start按钮访问。如果需要回溯分析,工具栏中的前进和后退按钮非常有用。

类视图 (Classes View)

堆遍历器顶部的视图选择器包含五个视图,分别展示当前对象集的不同信息。其中第一个就是“类 (Classes)”视图。

类视图类似于实时内存部分的“所有对象”视图,并带有聚合级别 (aggregation level) 选择器,可将类按包分组。此外,还可以显示类的估算保留大小 (retained size)。这表示如果从堆中移除该类的所有实例,将释放的内存量。点击Calculate estimated retained sizes超链接后,会新增一个Retained Size列。显示的保留大小是估算的下界,精确计算会非常慢。如果确实需要精确数值,可选中感兴趣的类或包,并在新对象集头部使用Calculate retained and deep sizes超链接。

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

解决类加载器 (classloader) 相关问题时,通常需要按类加载器分组实例。Inspections标签页提供了“按类加载器分组 (Group by class loaders)”的检查项 (inspection),在类视图中特别重要。如果执行该分析,顶部会显示一个分组表,列出所有类加载器。选择某个类加载器后,下方视图中的数据会相应过滤。切换到堆遍历器的其他视图时,分组表会保留,直到你执行新的选择步骤。此时,类加载器选择会成为该选择步骤的一部分。

分配记录视图 (Allocation Recording Views)

对象的分配位置对于定位内存泄漏或减少内存消耗非常重要。对于JProfiler堆快照,“分配 (Allocations)”视图会显示分配调用树 (allocation call tree) 及分配热点 (allocation hot spots),仅针对已记录分配的对象。其他对象会被归入分配调用树中的“未记录对象 (unrecorded objects)”节点。对于HPROF/PHD快照,该视图不可用。

与类视图类似,你可以多选节点,并使用顶部的Use Selected按钮创建新的选择步骤。在“分配热点 (Allocation hot spots)”视图模式下,还可以在回溯 (backtraces) 中选择节点。这样只会选中在所选回溯结尾的调用栈上分配的顶级热点对象。

JProfiler在记录分配时还可以保存对象的分配时间。“时间 (Time)”视图会显示当前对象集所有已记录实例的分配时间直方图。你可以点击并拖动选择一个或多个区间,然后通过Use Selected按钮创建新对象集。

若需更精确地选择时间区间,可以指定书签 (bookmark)范围。所有位于第一个和最后一个选中书签之间的对象都会被标记。

除了时间视图外,分配时间还会作为单独的列显示在引用视图中。但分配时间记录默认未启用。你可以在时间视图中直接开启,或在会话设置对话框的Advanced Settings -> Memory Profiling中修改该设置。

最大对象视图 (Biggest Objects View)

最大对象视图会列出当前对象集中最重要的对象。此处“最大”指的是如果移除这些对象,将释放最多内存的对象。该大小称为保留大小 (retained size)。相比之下,深度大小 (deep size)是通过强引用可达的所有对象的总大小。

每个对象都可以展开,显示其对其他被保留对象的出引用。你可以递归展开被保留对象的树,如果移除某个祖先对象,这些对象将被垃圾回收。这种树结构称为“支配树 (dominator tree)”。树中每个对象显示的信息类似于出引用视图,只是只显示支配引用。

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

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)]”。

在支配树每个引用节点的左侧,会有一个大小条,显示目标对象仍保留顶级对象保留大小的百分比。随着你深入树结构,数值会逐步减少。在视图设置中,可以将百分比基准切换为整个堆的总大小。

支配树内置了一个截止阈值,会过滤掉所有保留大小低于父对象保留大小0.5%的对象,以避免出现过长的无关小对象列表。如果发生这种过滤,会显示一个特殊的“cutoff”子节点,告知未显示对象的数量、总保留大小以及单个对象的最大保留大小。

支配树除了可以显示单个对象外,还可以将最大对象按类分组。视图顶部的分组下拉框有一个复选框可激活该显示模式。此外,还可以在顶层添加类加载器分组。类加载器分组在计算最大对象后应用,显示最大对象所属类的加载者。如果只想分析某个特定类加载器的最大对象,可以先使用“按类加载器分组”检查项。

最大对象视图上方的视图模式选择器允许你切换到旭日图 (sunburst diagram)。该图由一系列同心分段圆环组成,能在一张图中展示支配树的全部内容(有最大深度限制)。引用从最内圈发出,向外圈传播。这种可视化方式信息密度高,能帮助你发现引用模式,并通过特殊着色 (colorization) 一眼识别大型原始数组和对象数组。

如果当前对象集是整个堆,圆的总周长对应于已用堆大小。由于最大对象视图只显示保留超过总堆0.1%的对象,因此会有一大块空白区域,对应未被这些最大对象保留的所有对象。

点击任意圆环段会将其设为新的圆心,从而扩展你在图中可见的最大深度。点击图的空心中心可恢复上一个圆心。如果设置了新圆心,圆的总周长对应于根对象的保留大小。空白区域表示根对象的自有大小 (self-size) 及未出现在最大保留对象列表中的其他对象。如果当前对象集不是整个堆,圆的总周长对应于所有显示的最大对象之和,不会有空白区域。

将鼠标悬停在图中实例或其直接保留对象上,右侧会显示更多信息。当鼠标不在任何圆环段上时,右侧列表显示最内圈的最大对象。悬停在该列表上会高亮对应的圆环段,点击列表项会将其设为新圆心。要创建新对象集,可以在圆环段或列表项的右键菜单中选择操作。

引用视图 (Reference Views)

与前述视图不同,引用视图仅在你执行过至少一个选择步骤后才可用。对于初始对象集,这些视图并无意义,因为入引用和出引用视图会显示所有单个对象,合并引用视图只有在聚焦对象集时才有意义。

出引用视图类似于IDE调试器中的对象视图。展开对象时,可以看到原始数据和对其他对象的引用。任何引用类型都可以作为新对象集选中,也可以一次多选对象。与类视图类似,可以选择保留对象或关联的java.lang.Class对象。如果选中的是标准集合,还可以一键选中所有包含的元素。对于类加载器对象,还可以选中所有已加载实例。

默认情况下,不显示引用为null的字段,因为这些信息可能会干扰内存分析。如果需要调试时查看所有字段,可以在视图设置中更改该行为。

除了简单地选择显示的实例外,出引用视图还具备强大的过滤能力。对于实时会话 (live session),出引用和入引用视图都具备高级操作和显示功能,详见本章后续内容。

入引用视图是解决内存泄漏的主要工具。要查找对象为何未被垃圾回收,可点击Show Paths To GC Root按钮,查找到垃圾回收根的引用链。关于该主题的详细信息,请参见内存泄漏章节

合并引用 (Merged References)

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

你也可以选择按字段分组显示合并引用。此时,每个节点代表一种引用类型,如类的某个字段或数组内容。对于标准集合,会压缩内部引用链,避免影响聚合,因此你会看到类似“java.lang.HashMap的map value”这样的引用类型。与类聚合不同,该机制仅适用于JRE标准库中明确支持的集合。

在“合并出引用 (Merged outgoing references)”视图中,实例计数指的是被引用对象的数量。在“合并入引用 (Merged incoming references)”视图中,每行会显示两个实例计数。第一个计数表示当前对象集中有多少实例通过该路径被引用。节点左侧的条形图可视化该比例。第二个计数(箭头图标后)表示持有对父节点引用的对象数量。执行选择步骤时,可以选择是选中当前对象集中以该方式被引用的对象,还是选中持有该引用的对象(引用持有者)。

“合并支配引用 (Merged dominating references)”视图可帮助你找出必须移除哪些引用,才能让当前对象集中的部分或全部对象被垃圾回收。支配引用树可视为最大对象视图中支配树的合并逆向,按类聚合。引用箭头未必表示两个类之间有直接引用,中间可能还有其他类持有非支配引用。如果存在多个垃圾回收根,当前对象集中部分或全部对象可能不存在支配引用。

默认情况下,“合并支配引用”视图显示入支配引用,展开树后可到达被GC根持有的对象。有时,引用树可能通过多条路径指向同一根对象。通过在视图顶部下拉框选择“GC roots to objects”视图模式,可以切换到反向视角,将根对象置于顶层,当前对象集的对象位于叶节点。此时,引用从顶层指向叶节点。哪种视角更合适,取决于你要消除的引用是更靠近当前对象集还是更靠近GC根。

检查项 (Inspections)

“检查项 (Inspections)”视图本身不显示数据,而是提供多种堆分析方式,根据特定规则创建新对象集,这些规则在其他视图中不可用。例如,你可能想查看所有被线程本地变量 (thread local) 保留的对象,这在引用视图中无法实现。检查项分为多个类别,具体说明见其描述。

检查项可以将计算得到的对象集划分为多个组。组会在堆遍历器顶部的表格中显示。例如,“重复字符串 (Duplicate strings)”检查项会将重复的字符串值作为分组。如果你在引用视图中,可以看到选中字符串值下方的java.lang.String实例。初始时,分组表的第一行被选中。更改选择后,当前对象集也会随之变化。分组表的Instance CountSize列显示选中行对应的当前对象集的大小。

分组选择不是堆遍历器中的独立选择步骤,而是成为检查项选择步骤的一部分。你可以在底部的选择步骤面板中看到分组选择。更改分组选择时,选择步骤面板会立即更新。

每个创建分组的检查项会根据上下文决定哪些分组最重要。由于这不总是与其他列的自然排序一致,分组表的Priority列会包含一个数值,以强制指定检查项的排序顺序。

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

堆遍历器图形 (Heap Walker Graph)

最真实地展现实例及其引用关系的方式是图形 (graph)。虽然图形的可视密度较低,对某些分析类型不太实用,但它仍然是可视化对象关系的最佳方式。例如,循环引用在树结构中难以识别,但在图形中一目了然。此外,同时查看入引用和出引用也是树结构无法实现的,而图形可以。

堆遍历器图形不会自动显示当前对象集中的任何对象,也不会在更改当前对象集时自动清空。你需要在出引用视图、入引用视图或最大对象视图中手动选中实例,并通过Show In Graph操作将其添加到图形中。

图形中的包名默认会被缩短。与CPU调用图 (call graph) 类似,你可以在视图设置中启用完整显示。引用以箭头形式绘制。将鼠标悬停在引用上,会弹出工具提示窗口,显示该引用的详细信息。从引用视图手动添加的实例背景为蓝色,越新添加颜色越深。垃圾回收根为红色背景,类为黄色背景。

默认情况下,引用图只显示当前实例的直接入引用和出引用。你可以通过双击对象扩展图形,这会根据你操作的方向展开该对象的直接入引用或出引用。通过实例左右两侧的扩展控件,可以选择性地展开入引用或出引用。如果需要回溯,可使用撤销功能恢复图形的先前状态,避免被过多节点干扰。要修剪图形,可使用移除所有未连接节点或全部对象的操作。

与入引用视图类似,图形中有Show Path To GC Root按钮,可展开到垃圾回收根的一条或多条引用链(如有)。此外,还有Find Path Between Two Selected Nodes操作(当选中两个实例时可用),可查找有向或无向路径,并可选择是否沿弱引用查找。如果找到合适路径,会以红色高亮显示。

初始对象集 (Initial Object Set)

获取堆快照时,可以指定控制初始对象集的选项。如果已记录分配,Select recorded objects复选框会将初始显示对象限制为已记录的对象。数量通常与实时内存视图不同,因为堆遍历器会移除无引用对象。未记录对象仍然存在于堆快照中,只是不会在初始对象集中显示。通过后续选择步骤仍可访问未记录对象。

此外,堆遍历器会执行一次垃圾回收,并移除弱引用对象(软引用除外)。这通常是期望的,因为弱引用对象在查找只与强引用相关的内存泄漏时会干扰分析。但如果你需要分析弱引用对象,可以让堆遍历器保留它们。JVM中的四种弱引用类型为“soft”、“weak”、“phantom”和“finalizer”,你可以选择哪些类型足以让对象在堆快照中被保留。

如果存在弱引用对象,可以通过堆遍历器中的“Weak reference”检查项将其选中或从当前对象集中移除。

堆标记 (Marking the Heap)

通常你希望查看为特定用例分配的对象。虽然可以在该用例前后手动开启和关闭分配记录,但有一种更高效且不会影响分配记录功能的方式:Mark Heap操作。该操作在堆遍历器概览页有提示,也可在Profiling菜单或作为触发器 (trigger) 操作使用。它会将堆中的所有对象标记为“旧 (old)”。下次获取堆快照时,“新 (new)”对象就很容易区分了。

如果之前有堆快照或执行过Mark Heap,堆遍历器标题区域会显示新实例数量,并提供Use newUse old两个链接,允许你选择自那时起分配的实例,或之前分配且仍存活的实例。每个对象集都可获得该信息,因此你可以先深入分析,再选择新或旧实例。