堆遍历器
堆快照
任何涉及对象之间引用的堆分析都需要堆快照,因为无法询问JVM对象的传入引用是什么。您必须遍历整个堆来回答这个问题。从该堆快照中,JProfiler创建了一个内部数据库,该数据库经过优化以生成堆遍历器中视图所需的数据。
堆快照有两个来源:JProfiler堆快照和HPROF/PHD堆快照。JProfiler堆快照支持堆遍历器中的所有可用功能。分析代理使用分析接口JVMTI遍历所有引用。如果被分析的JVM在不同的机器上运行,所有信息将传输到本地机器,并在那里进行进一步计算。HPROF/PHD快照是通过JVM中的内置机制创建的,并以JProfiler可以读取的标准格式写入磁盘。HotSpot JVM可以创建HPROF快照,而Eclipse OpenJ9 JVM提供PHD快照。
在堆遍历器的概览页面上,您可以选择是创建JProfiler堆快照还是HPROF/PHD堆快照。默认情况下,推荐使用JProfiler堆快照。HPROF/PHD堆快照在另一章中讨论的特殊情况下很有用。
选择步骤
堆遍历器由几个视图组成,这些视图显示所选对象集的不同方面。拍摄堆快照后,您正在查看堆上的所有对象。每个视图都有导航操作,用于将一些选定的对象转换为当前对象集。堆遍历器的标题区域显示当前对象集中包含多少个对象的信息。
最初,您正在查看“Classes”视图,该视图类似于live memory section中的“All objects”视图。通过选择一个类并调用Use→Selected Instances,您可以创建一个仅包含该类实例的新对象集。在堆遍历器中,“使用”总是意味着创建一个新的对象集。
对于新的对象集,显示堆遍历器的类视图并不有趣,因为它实际上只是将表过滤到先前选择的类。相反,JProfiler建议使用“New object set”对话框中的另一个视图。您可以取消此对话框以放弃新对象集并返回到上一个视图。建议使用传出引用视图,但您也可以选择其他视图。这只是最初显示的视图,您可以在堆遍历器的视图选择器中切换视图。
标题区域现在告诉您有两个选择步骤,并包括用于计算保留和深度大小或使用当前对象集保留的所有对象的链接。后者将添加另一个选择步骤,并建议类视图,因为在该对象集中可能会有多个类。
在堆遍历器的下部,列出了到目前为止的选择步骤。单击超链接将带您返回到任何选择步骤。第一个数据集也可以通过工具栏中的Go To Start按钮到达。如果您需要回溯分析,工具栏中的后退和前进按钮非常有用。
Classes视图
堆遍历器顶部的视图选择器包含五个视图,这些视图显示当前对象集的不同信息。其中第一个是“Classes”视图。
类视图类似于live memory section中的“All objects”视图,并具有可以将类分组到包中的聚合级别选择器。此外,它可以显示类的估计保留大小。这是如果从堆中删除一个类的所有实例,将释放的内存量。如果单击Calculate estimated retained sizes超链接,将添加一个新的Retained Size列。显示的保留大小是估计的下限,计算确切的数字会太慢。如果您确实需要确切的数字,请选择感兴趣的类或包,并使用新对象集标题中的Calculate retained and deep sizes超链接。
基于您选择的一个或多个类或包,您可以选择实例本身、关联的java.lang.Class
对象或所有保留的对象。双击是最快的选择模式,并使用选定的实例。如果有多个选择模式可用,如在这种情况下,视图上方将显示一个Use下拉菜单。
在解决类加载器相关问题时,您通常需要按类加载器对实例进行分组。Inspections选项卡提供了一个“Group by class loaders”检查,该检查在类视图中可用,因为在该上下文中特别重要。如果您执行该分析,顶部的分组表将显示所有类加载器。选择一个类加载器会相应地过滤下面视图中的数据。当您切换到堆遍历器的其他视图时,分组表仍然存在,直到您执行另一个选择步骤。然后,类加载器选择成为该选择步骤的一部分。
Allocation recording视图
当缩小内存泄漏嫌疑对象或尝试减少内存消耗时,了解对象的分配位置可能很重要。对于JProfiler堆快照,“Allocations”视图显示分配调用树和分配热点,适用于那些记录了分配的对象。其他对象在分配调用树中被分组到“unrecorded objects”节点中。对于HPROF/PHD快照,此视图不可用。
与类视图一样,您可以选择多个节点,并使用顶部的Use Selected按钮创建新的选择步骤。在“Allocation hot spots”视图模式中,您还可以选择回溯中的节点。这将仅选择在关联的顶级热点上分配的对象,这些对象的调用栈以所选回溯结束。
JProfiler在记录分配时可以保存的另一条信息是对象分配的时间。堆遍历器中的“Time”视图显示当前对象集中所有记录实例的分配时间直方图。您可以单击并拖动以选择一个或多个间隔,然后使用Use Selected按钮创建一个新的对象集。
为了更精确地选择时间间隔,您可以指定bookmarks的范围。所有在第一个和最后一个选定书签之间的对象将被标记。
除了时间视图,分配时间还显示为引用视图中的单独列。然而,默认情况下不启用分配时间记录。您可以直接在时间视图中打开它,或者在会话设置对话框中的Advanced Settings -> Memory Profiling中编辑设置。
Biggest objects视图
最大对象视图显示当前对象集中最重要对象的列表。在此上下文中,“最大”意味着如果从堆中删除这些对象,将释放最多内存的对象。该大小称为retained size。相反,deep size是通过强引用可达的所有对象的总大小。
每个对象都可以展开以显示由该对象保留的其他对象的传出引用。通过这种方式,您可以递归展开保留对象的树,如果删除其中一个祖先,这些对象将被垃圾回收。这种树称为“dominator tree”。在此树中显示的每个对象的信息类似于传出引用视图,只是仅显示主导引用。
并非所有被支配的对象都直接由其支配者引用。例如,考虑下图中的引用:
对象A支配对象B1和B2,并且它没有直接引用对象C。B1和B2都引用C。B1和B2都不支配C,但A支配。在这种情况下,B1、B2和C被列为A在支配树中的直接子节点,C不会被列为B1和B2的子节点。对于B1和B2,显示它们在A中被持有的字段名称。对于C,在引用节点上显示“[transitive reference]”。
在支配树的每个引用节点的左侧,大小条显示顶级对象的保留大小中仍由目标对象保留的百分比。随着您进一步深入树中,数字将减少。在视图设置中,您可以将百分比基数更改为总堆大小。
支配树有一个内置的截止点,消除了所有保留大小低于父对象保留大小0.5%的对象。这是为了避免过长的小支配对象列表,这些对象会分散对重要对象的注意力。如果发生这样的截止,将显示一个特殊的“cutoff”子节点,通知您在此级别上未显示的对象数量、它们的总保留大小以及单个对象的最大保留大小。
除了显示单个对象,支配树还可以将最大对象分组到类中。视图顶部的分组下拉菜单包含一个复选框,用于激活此显示模式。此外,您可以在顶级添加类加载器分组。类加载器分组在计算最大对象后应用,并显示谁加载了最大对象的类。如果您想分析特定类加载器的最大对象,可以先使用“Group by class loader”检查。
最大对象视图上方的视图模式选择器允许您切换到旭日图。该图由一系列同心分段环组成,显示支配树的整个内容,最多深度为一个图像。引用起源于最内层的环,并向圆的外缘传播。此可视化为您提供了一个扁平化的视角,具有高信息密度,使您能够发现引用模式,并通过其特殊的颜色编码一目了然地看到大型原始和对象数组。
如果当前对象集是整个堆,则圆的总周长对应于已用堆大小。因为最大对象视图仅显示保留超过总堆0.1%的对象,这意味着一个重要的扇区将是空的,对应于所有未被这些最大对象保留的对象。
单击任何环段都会为圆设置一个新的根,从而扩展您在图中可以看到的最大深度。单击图的空心中心会恢复先前的根。如果设置了新根,则圆的总周长对应于根对象的保留大小。空扇区表示根对象的自大小和在最大保留对象列表中不存在的其他对象。如果当前对象集不是整个堆,则圆的总周长对应于所有显示的最大对象的总和,并且不会显示空扇区。
当您将鼠标悬停在它们上面时,图的右侧会显示有关实例及其立即保留对象的更多信息。当鼠标不在任何环段上时,右侧的列表显示最内层环中的最大对象。将鼠标悬停在该列表上会突出显示相应的环段,单击列表项会为图设置一个新根。要创建一个新的对象集,您可以从上下文菜单中的操作中选择,无论是在环段上还是在列表项上。
Reference视图
与之前的视图不同,引用视图仅在您执行至少一个选择步骤后可用。对于初始对象集,这些视图没有用,因为传入和传出引用视图显示所有单个对象,并且合并引用视图只能针对专注的对象集进行解释。
传出引用视图类似于IDE中的调试器会显示的视图。打开对象时,您可以看到原始数据和对其他对象的引用。任何引用类型都可以选择为新的对象集,您可以一次选择多个对象。与类视图一样,您可以选择保留的对象或关联的java.lang.Class
对象。如果选定的对象是标准集合,您还可以通过单个操作选择所有包含的元素。对于类加载器对象,有一个选项可以选择所有加载的实例。
默认情况下,不显示具有空引用的字段,因为该信息可能会分散内存分析的注意力。如果您想出于调试目的查看所有字段,可以在视图设置中更改此行为。
除了简单选择显示的实例,传出引用视图还具有 强大的过滤功能。 对于实时会话,传出和传入引用视图都具有高级的操作和显示功能,在同一章中进行了讨论。
传入引用视图是解决内存泄漏的主要工具。要找出为什么对象没有被垃圾回收,Show Paths To GC Root按钮将找到到垃圾收集器根的引用链。关于这个重要主题的详细信息,请参见内存泄漏章节。
Merged references
检查许多不同对象的引用可能很繁琐,因此JProfiler可以向您显示当前对象集中所有对象的合并传出和传入引用。默认情况下,引用按类聚合。如果一个类的实例被同一类的其他实例引用,则插入一个
您还可以选择按字段分组显示合并引用。在这种情况下,每个节点都是一个引用类型,例如类的特定字段或数组的内容。对于标准集合,内部引用链会破坏累积,因此您会看到类似“map value of java.lang.HashMap”的引用类型。与类聚合不同,此机制仅适用于JRE标准库中显式支持的集合。
在“Merged outgoing references”视图中,实例计数指的是引用的对象。在“Merged incoming references”视图中,您会在每行看到两个实例计数。第一个实例计数显示当前对象集中沿此路径引用的实例数量。节点左侧的条形图标可视化此比例。箭头图标后的第二个实例计数指的是持有对父节点引用的对象。在执行选择步骤时,您可以选择是要选择当前对象集中以选定方式引用的对象,还是对选定引用感兴趣的对象——引用持有者。
使用“Merged dominating references”视图,您可以找出必须删除哪些引用,以便当前对象集中的某些或所有对象可以被垃圾回收。支配引用树可以解释为最大对象视图中支配树的合并逆,按类聚合。引用箭头可能不表示两个类之间的直接引用,但可能有其他类在中间持有非支配引用。在多个垃圾收集器根的情况下,可能没有支配引用存在于当前对象集中的某些或所有对象。
默认情况下,“Merged dominating references”视图显示传入的支配引用,通过打开树,您可以到达由GC根持有的对象。有时,引用树可能通过许多不同的路径引导到相同的根对象。通过在视图顶部的下拉菜单中选择“GC roots to objects”视图模式,您可以看到反向视角,其中根位于顶级,当前对象集中的对象位于叶节点。在这种情况下,引用从顶级向叶节点传播。哪个视角更好取决于您要消除的引用是接近当前对象集还是接近GC根。
Inspections
“Inspections”视图本身不显示数据。它提供了一些堆分析,根据其他视图中不可用的规则创建新的对象集。例如,您可能想查看由线程本地保留的所有对象。这在引用视图中是不可能做到的。检查被分为几个类别,并在其描述中进行解释。
检查可以将计算的对象集划分为组。组显示在堆遍历器顶部的表中。例如,“Duplicate
strings”检查将重复的字符串值显示为组。如果您在引用视图中,您可以看到具有选定字符串值的java.lang.String
实例。最初,组表中的第一行被选中。通过更改选择,您更改了当前对象集。组表的Instance
Count和Size列告诉您选择一行时当前对象集的大小。
组选择不是堆遍历器中的单独选择步骤,但它成为检查所做选择步骤的一部分。您可以在底部的选择步骤窗格中看到组选择。当您更改组选择时,选择步骤窗格会立即更新。
每个创建组的检查决定哪些组在检查的上下文中最重要。因为这并不总是与其他列的自然排序顺序相对应,组表中的Priority列包含一个数值,用于强制检查的排序顺序。
对于大堆,检查可能计算成本高,因此结果会被缓存。这样,您可以回到历史记录中查看先前计算的检查结果,而无需等待。
Heap walker graph
实例及其引用的最真实表示是图形。虽然图形的视觉密度低,并且对于某些类型的分析不实用,但它仍然是可视化对象之间关系的最佳方式。例如,循环引用在树中难以解释,但在图中立即显而易见。此外,看到传入和传出引用可能是有益的,这在树结构中是不可能的,在树结构中您可以看到其中之一。
堆遍历器图形不会自动显示当前对象集中的任何对象,也不会在您更改当前对象集时清除。您可以从传出引用视图、传入引用视图或最大对象视图中手动将选定对象添加到图形中,方法是选择一个或多个实例并使用Show In Graph操作。
默认情况下,图中的包名称会缩短。与CPU调用图一样,您可以在视图设置中启用完整显示。引用以箭头形式绘制。如果您将鼠标移动到引用上,将显示一个工具提示窗口,显示特定引用的详细信息。从引用视图手动添加的实例具有蓝色背景。实例添加得越晚,背景颜色越深。垃圾收集器根具有红色背景,类具有黄色背景。
默认情况下,引用图仅显示当前实例的直接传入和传出引用。您可以通过双击任何对象来扩展图形。这将根据您移动的方向,展开该对象的直接传入或直接传出引用。使用实例左右两侧的扩展控件,您可以选择性地打开传入和传出引用。如果您需要回溯,请使用撤销功能恢复图形的先前状态,以免被太多节点分散注意力。要修剪图形,有删除所有未连接节点或甚至删除所有对象的操作。
与传入引用视图一样,图形具有Show Path To GC Root按钮,如果可用,将扩展到垃圾收集器根的一个或多个引用链。此外,还有一个Find Path Between Two Selected Nodes操作,如果选择了两个实例,则处于活动状态。它可以搜索有向和无向路径,并可选地沿着弱引用。如果找到合适的路径,将以红色显示。
Initial object set
当您拍摄堆快照时,您可以指定控制初始对象集的选项。如果您记录了分配,Select recorded objects复选框将初始显示的对象限制为那些已记录的对象。数字通常会与live memory视图中的数字不同,因为堆遍历器会删除未引用的对象。未记录的对象仍然存在于堆快照中,只是没有显示在初始对象集中。通过进一步的选择步骤,您可以到达未记录的对象。
此外,堆遍历器会执行垃圾收集并删除弱引用对象,软引用除外。这通常是可取的,因为在寻找内存泄漏时,弱引用对象会分散注意力,而只有强引用对象才相关。然而,在那些您对弱引用对象感兴趣的情况下,您可以告诉堆遍历器保留它们。JVM中的四种弱引用类型是“soft”、“weak”、“phantom”和“finalizer”,您可以选择其中哪种类型应该足以在堆快照中保留对象。
如果存在,弱引用对象可以通过使用堆遍历器中的“Weak reference”检查从当前对象集中选择或删除。
Marking the heap
通常,您希望查看为特定用例分配的对象。虽然您可以通过在该用例周围启动和停止分配记录来实现,但有一种更好的方法,具有更少的开销,并为其他目的保留分配记录功能:Mark Heap操作,该操作在堆遍历器概览中进行宣传,并且也可以在Profiling菜单中或作为触发器操作使用,将堆上的所有对象标记为“旧”。当您拍摄下一个堆快照时,现在很清楚“新”对象应该是什么。
如果存在先前的堆快照或标记堆调用,堆遍历器的标题区域会显示新的实例计数和两个标题为Use new和Use old的链接,允许您选择自那时起分配的实例,或在此之前分配的幸存实例。此信息适用于每个对象集,因此您可以先深入研究,然后选择新实例或旧实例。