JProfiler帮助文档

内存分析 (Memory profiling)

获取堆上对象信息有两种方式。一方面,分析代理可以跟踪每个对象的分配 (allocation) 和垃圾回收 (garbage collection)。在 JProfiler 中,这被称为“分配记录 (allocation recording)”。它可以告诉你对象是在哪里被分配的,并且还可以用于统计临时对象。另一方面,JVM 的分析接口允许分析代理获取“堆快照 (heap snapshot)”,以便检查所有存活对象及其引用。这些信息对于理解对象为何无法被垃圾回收是必需的。

分配记录和堆快照都是开销较大的操作。分配记录对运行时特性有较大影响,因为 java.lang.Object 构造方法必须被插桩 (instrumented),而垃圾回收器需要持续向分析接口报告。这也是为什么默认不会记录分配,你需要显式地开始和停止记录。获取堆快照是一次性操作,但它可能会让 JVM 暂停几秒钟,并且分析获取到的数据可能需要较长时间,具体取决于堆的大小。

JProfiler 将其内存分析分为两个视图 (view) 区块:“实时内存 (Live memory)”区块展示可以定期更新的数据,而“堆遍历器 (Heap walker)”区块则展示静态堆快照。分配记录在“实时内存”区块中控制,但记录的数据也会在堆遍历器中展示。

内存分析可以解决的三种最常见问题是:查找内存泄漏 (memory leak)、减少内存消耗,以及减少临时对象的创建。前两种问题主要使用堆遍历器,通常通过查看 JVM 中哪些对象被持有以及它们的创建位置来定位。最后一种问题只能依赖展示已记录分配的实时视图,因为涉及的对象可能已经被垃圾回收。

实例数量跟踪 (Tracking instance counts)

为了总览堆上的对象,“所有对象 (All objects)”视图会以直方图形式展示所有类及其实例数量。该视图中的数据不是通过分配记录收集的,而是通过执行一个只计算实例数量的小型堆快照获得。堆越大,执行该操作所需时间越长,因此该视图不会自动用当前值更新。

查找内存泄漏时,你通常希望比较不同时间点的实例数量。要对所有类进行比较,可以使用该视图的差异功能。当同时选择两个“所有对象”快照时,会插入一个差异 (Difference)列,实例数量的直方图会以绿色显示标记时的基线值。当获取新的“所有对象”快照时,最早被选中的快照会保持选中状态,并显示与新快照的差异。

快照选择器会显示快照获取时的时间戳。双击快照可以添加标签,便于识别。你也可以通过触发器 (trigger) 操作Controller API来触发“所有对象”快照,并可指定标签。

另一方面,“已记录对象 (Recorded objects)”视图只显示你开始分配记录后分配的对象实例数量。当你停止分配记录时,不会再添加新的分配,但垃圾回收仍会被跟踪。这样,你可以看到某个用例下哪些对象留在了堆上。注意,对象可能很长时间不会被垃圾回收。你可以通过工具栏的运行 GC (Run GC)按钮加快该过程。对于大多数动态更新的视图,工具栏上都提供了冻结 (Freeze)按钮,用于停止数据更新。

使用工具栏的标记当前 (Mark Current)按钮,你也可以在“已记录对象”视图中显示与选定基线的差异列。对于选中的类,还可以通过右键菜单的添加选择到类追踪器 (Add Selection to Class Tracker)操作,显示该类的时间变化图形 (graph)。

分配点 (Allocation spots)

当分配记录处于激活状态时,JProfiler 会在每次对象分配时记录调用栈 (call stack)。它不会使用如 stack-walking API 那样的精确调用栈,因为那样开销太大。相反,采用与 CPU 分析相同的机制。这意味着调用栈会根据调用树过滤器 (call tree filters)进行过滤,实际的分配点 (allocation spot) 可能位于未出现在调用栈中的方法中,因为它属于被忽略或被压缩过滤的类。不过,这些变化很容易理解:被压缩过滤的方法负责其后续对压缩过滤类的所有分配。

如果你使用采样 (sampling),分配点会变得近似且可能令人困惑。与时间测量不同,你通常很清楚某些类可以在哪里被分配,哪里不能。由于采样呈现的是统计而非精确结果,你可能会看到看似不可能的分配点,比如 java.util.HashMap.get 分配了你自己的类。对于需要精确数字和调用栈的分析,建议结合插桩 (instrumentation) 使用分配记录。

与 CPU 分析类似,分配调用栈以调用树 (call tree) 形式展示,只不过是分配次数和分配内存,而不是调用次数和时间。与 CPU 调用树不同,分配调用树不会自动显示和更新,因为树的计算开销更大。JProfiler 不仅可以为所有对象显示分配树,还可以为选定的类或包显示。结合其他选项,这些都可以在你请求 JProfiler 从当前数据计算分配树后弹出的选项对话框中配置。

CPU 调用树的一个有用特性是你可以自上而下跟踪累计时间,因为每个节点包含其子节点消耗的时间。默认情况下,分配树也采用相同方式,即每个节点包含其子节点产生的分配。即使分配只发生在调用树底部的叶子节点,数值也会向上传递到顶部。这样,你在展开分配调用树分支时总能看到值得关注的路径。“自分配 (Self-allocations)”指的是由当前节点实际执行的分配,而不是其子节点。与 CPU 调用树一样,百分比条会用不同颜色显示自分配。

在分配调用树中,常常有很多节点本身没有发生分配,尤其是在为选定类显示分配时。这些节点只是为了展示实际分配发生节点的调用栈路径。此类节点在 JProfiler 中称为“桥接 (bridge)”节点,并以灰色图标显示,如上图所示。在某些情况下,分配的累计会影响分析,你可能只想看到实际的分配点。分配树的视图设置对话框提供了显示未累计数值的选项。启用后,桥接节点始终显示为零分配且没有百分比条。

分配热点 (hot spot) 视图与分配调用树一起生成,允许你直接聚焦于负责创建选定类的方法。与已记录对象视图类似,分配热点视图支持标记当前状态并观察随时间的变化。视图中会新增一个差异列,显示自标记当前值 (Mark Current Values)操作被调用以来热点的变化。由于分配视图默认不会定期更新,你需要点击计算 (Calculate)工具栏按钮获取新数据集,并与基线值进行比较。选项对话框中可启用自动更新,但对于大堆内存不推荐使用。

分配记录速率 (Allocation recording rate)

记录每一次分配会带来显著的性能开销。在很多情况下,分配的总数并不重要,相对数值已足够解决问题。因此,JProfiler 默认只记录每第 10 次分配。这将开销降低到记录全部分配的约 1/10。如果你希望记录所有分配,或者更少的分配已满足需求,可以在已记录对象视图以及分配调用树和热点视图的参数对话框中更改记录速率。

该设置也可以在会话设置对话框的“高级设置->内存分析 (Memory profiling)”步骤中找到,并可针对离线分析会话进行调整。

分配记录速率会影响“已记录对象 (Recorded objects)”和“已记录吞吐量 (Recorded throughput)”的 VM 遥测 (telemetry),其数值会按配置比例进行测量。当比较快照 (snapshots)时,会报告第一个快照的分配速率,其他快照会相应缩放(如有必要)。

已分配类分析 (Analyzing allocated classes)

计算分配树和分配热点视图时,你需要预先指定希望查看分配的类或包。如果你已经关注某些特定类,这种方式很方便,但在没有先验假设时查找分配热点就不太方便。一种方法是先查看“已记录对象”视图,并通过右键菜单操作切换到选定类或包的分配树或分配热点视图。

另一种方法是从所有类的分配树或分配热点视图开始,使用显示类 (Show classes)操作,显示选定分配点或分配热点对应的类。

已分配类的直方图以调用树分析 (call tree analysis)的形式展示。该操作也适用于其他调用树分析。

类分析视图是静态的,在分配树和热点视图重新计算时不会自动更新。重新加载分析 (Reload Analysis)操作会先更新分配树,然后用新数据重新计算当前分析视图。

已回收对象分析 (Analyzing garbage collected objects)

分配记录不仅可以显示存活对象,还会保留已被垃圾回收对象的信息。这在分析临时分配时非常有用。大量临时对象的分配会带来显著开销,因此降低分配速率通常能大幅提升性能。

要在已记录对象视图中显示已回收对象,请将活性 (liveness) 选择器切换为已回收对象 (Garbage collected objects)存活和已回收对象 (Live and garbage collected objects)。分配调用树和分配热点视图的选项对话框中也有类似下拉选项。

但 JProfiler 默认不会为已回收对象收集分配树信息,因为仅维护存活对象的数据开销要小得多。当你在“分配调用树 (Allocation Call Tree)”或“分配热点 (Allocation Hotspots)”视图中将活性选择器切换为包含已回收对象的模式时,JProfiler 会建议更改记录类型。这会修改配置文件设置 (profiling setting),如果你选择立即应用,所有之前记录的数据都会被清除。如果你希望提前更改该设置,可以在“高级设置”->“内存分析 (Memory Profiling)”的会话设置对话框中进行。

下一站:堆遍历器 (Next stop: heap walker)

更高级的问题通常涉及对象之间的引用。例如,在已记录对象、分配树和分配热点视图中显示的大小是浅大小 (shallow sizes),只包括类的内存布局,不包括任何被引用的类。要了解某个类的对象实际占用多少内存,你通常需要知道保留大小 (retained size),即如果这些对象从堆中移除将释放的内存量。

这类信息在实时内存视图中无法获得,因为需要枚举堆上所有对象并进行复杂计算。这个任务由堆遍历器 (heap walker) 完成。要从实时内存视图的关注点跳转到堆遍历器,可以使用工具栏的在堆遍历器中显示 (Show in Heap Walker)按钮。它会带你进入堆遍历器中的对应视图。

如果没有可用的堆快照,将会创建一个新的堆快照,否则 JProfiler 会询问你是否使用现有的堆快照。

无论哪种情况,重要的是要理解实时内存视图和堆遍历器中的数值通常会有很大差异。除了堆遍历器展示的是不同时间点的快照外,它还会排除所有无引用对象。根据垃圾回收器的状态,无引用对象可能会占据堆的很大一部分空间。