Perfetto ART Heap Dump 深度指南:捕获 Java/Kotlin 堆引用图并分析内存泄漏
2026/9/17 17:26:20 网站建设 项目流程

Perfetto ART Heap Dump 深度指南:捕获 Java/Kotlin 堆引用图并分析内存泄漏

【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto

本指南以 Perfetto 仓库中的 docs/data-sources/java-heap-profiler.md 为骨架,系统讲解 ART(Android RunTime)堆转储(Heap Dump)这一数据源的完整使用链路:从捕获配置、UI 火焰图解读,到 trace_processor 的 SQL 表结构与 stdlib 标准库分析模块。读完本文,你将掌握如何在 Android 11+ 设备上为指定进程抓取 Java 堆的完整引用图,并用 SQL 定位真正"留住"内存的对象类型。

ART Heap Dump 是什么:引用图而非对象数据

捕获 Heap Dump 需要 Android 11 或更高版本。Perfetto 的 ART Heap Dump 与标准 JVM / HPROF 堆转储有本质区别:它只包含对象之间的引用图(reference graph),而不包含对象内部的数据内容。换句话说,一次 dump 记录下来的信息形如"对象 X 通过其名为 Z 的类成员,保留了对象 Y,Y 的大小为 N 字节",但不会携带字符串的实际值、数组的元素内容等数据。

这与 native heap profiler(原生堆分析)恰好互补:

维度ART Heap DumpNative Heap Profile
记录内容Java 对象完整保留图(retention graph)分配事件 / 调用栈
是否有调用栈
典型用途内存泄漏定位、对象留存分析分配热点、调用路径分析

Heap Dump 也不要与 ART Allocation Profiling 混淆——后者记录的是分配事件与调用栈(详见 native-heap-profiler.md 中的 ART Allocation Profiling 一节)。在动手之前,建议先阅读 内存案例分析指南,了解如何用dumpsys meminfo做高层内存概览、理解 Linux 内存管理(RSS / PSS / dirty page 等),再深入堆图分析。

捕获 Heap Dump:TraceConfig 配置详解

ART heap dump 数据源通过 trace config 中的JavaHprofConfig段配置,完整字段定义见源码 protos/perfetto/config/profiling/java_hprof_config.proto。原文档给出的最小可用配置如下:

data_sources { config { name: "android.java_hprof" java_hprof_config { process_cmdline: "com.google.android.inputmethod.latin" dump_smaps: true } } }

process_cmdline:进程匹配的语义演进

process_cmdline是命令行的白名单,按/proc/<pid>/cmdline(注意不是 comm 字符串)匹配。它的匹配语义随 Android 版本变化:

  • Android 13+(T):字段支持单个通配符*,有两种匹配方式:
    • 模式以/开头时,与 cmdline 的第一个段(即 argv0)匹配,例如/bin/e*可匹配/bin/echo
    • 否则与 argv0 中对应二进制名的部分匹配(与/proc/pid/exe无关),例如echo可匹配/bin/echo
  • Android 12(S)及以下:模式与/proc/pid/cmdline在比较前都会被归一化:先裁剪掉第一个 null 或@字节之后的内容;若字符串含斜杠,则裁剪到最后一个斜杠为止(即只保留二进制名)。

实现层面的一个细节:无论哪种方式,cmdline 最多只考虑 511 个字符。

其他常用字段

  • pid:按 PID 直接指定目标,适用于基于水印触发或本地调试。
  • target_installed_by(Android 12+):仅分析由给定包安装的目标应用,特殊值包括@system(系统分区)、@product(product 分区)、@null(侧载 sideload)。
  • continuous_dump_config:按固定间隔连续 dump。内部包含三个子字段:
    • dump_phase_ms:第一次连续 dump 前的等待毫秒数(trace 开始时总会先 dump 一次);
    • dump_interval_ms:后续每次 dump 的间隔毫秒数(不为 0 时启用连续 dump 配置);
    • scan_pids_only_on_start:为 true 时仅在数据源启动时扫描所有进程来匹配process_cmdline并按min_anonymous_memory_kb过滤(Android S- 的默认行为);为 false 时每次 dump 都重新扫描(Android T+ 的默认行为)。
  • min_anonymous_memory_kb:匿名 RSS + swap 小于该值的进程不分析。
  • min_java_heap_size_kb:Java 堆大小小于该值的进程不分析。
  • dump_smaps:是否包含进程的/proc/self/smaps。注意只展示满足以下条件之一的映射:以/system开头、以/vendor开头、以/data/app开头,或包含 "extracted in memory from Y"(其中 Y 匹配以上任一前缀)。在 perfetto v58+ 中被smaps_config取代
  • smaps_config(perfetto v58 新增):更精细的 smaps 内存映射配置。
  • ignored_types:从 profile 中排除指定类型,例如排除大量无意义的sun.misc.Cleaner对象。
  • dump_oome_callstack:仅对 OutOfMemoryError 触发的 heap dump 生效,转储触发 OOM 的线程调用点。

命令行快速捕获

除了手工构造 TraceConfig,仓库还提供了开箱即用的脚本 tools/java_heap_dump(使用说明见 docs/case-studies/memory.md):

$ tools/java_heap_dump -n com.android.systemui Dumping Java Heap. Wrote profile to /tmp/tmpup3QrQprofile This can be viewed using https://ui.perfetto.dev.

另外,当应用分配开始失败时,也可以在 本地 Android trace 录制 场景下采集 OOM 时的对象图快照。

UI 查看:从 Diamond 到火焰图

抓取到的 heap dump 会作为进程 track 中的ART heap dump轨道展示,每次 dump 对应一个菱形标记(diamond):

点击菱形后,UI 会呈现一组火焰图视图:

"Object Size" 与 "Object Count" 标签页

这两个视图展示的是到 GC 根最短路径上归属的内存。一个对象通常被多条路径可达,只展示最短路径是为了降低数据复杂度并保留最高信号量;最右侧的(merged)栈是太小而无法单独展示的对象的总和。

  • Object Size:经由此路径到 GC 根被保留的字节数。
  • Object Count:经由此路径到 GC 根被保留的对象个数。

可以用 Filters 框做类名过滤,例如输入notification只看与通知相关的所有分配。路径按类名聚合:若多个同类对象被同一个java.lang.Object[]保留,则只展示一个子元素。

"Dominated Object Size" 与 "Dominated Object Count" 标签页

这是将堆图按**支配树(dominator tree)**重新组织后的火焰图表示。在堆图中,对象a支配对象b,当且仅当b从根可达的所有路径都必须经过a。一个对象的所有支配者构成从根出发的一条链,该对象被这条链上的所有对象排他性保留;所有可达对象的这些链构成一棵树,即支配树。树路径同样按类名聚合,每个节点代表一组类名相同、在支配树中位置相同的对象。

  • Dominated Object Size:节点中对象排他性保留的字节数。
  • Dominated Object Count:节点中对象排他性保留的对象个数。

[native] 子节点:原生内存归属

某些对象的原生大小(native size)会在火焰图中以额外子节点呈现,节点前缀为[native],该额外节点会被计为额外一个对象。此能力仅 Android 13 及以上可用,数据来自libcore.util.NativeAllocationRegistry,且不计入self_size

SQL 分析:三张核心表

Java 堆信息被写入 trace_processor 的三张表(表结构定义见 src/trace_processor/tables/profiler_tables.py 中的 ART Heap Graphs 分组):

heap_graph_class

说明
name类名(可能被混淆)
deobfuscated_name若类名被混淆且提供了反混淆映射,则为反混淆后的名称
location类所在的 APK / Dex / JAR 文件
superclass_id父类(指向本表的 id)
classloader_id类加载器 id
kind类种类

heap_graph_object

同一(upid, graph_sample_ts)的所有行构成一次 dump:

说明
upid目标进程的唯一 PID
graph_sample_ts本次 dump 的时间戳
self_size该对象在 Java 堆上占用的字节数
native_size该对象关联的原生内存近似值(来自libcore.util.NativeAllocationRegistry.size
reference_set_idheap_graph_reference的 join key,包含该对象字段引用的所有对象
reachable该对象是否从 GC 根可达;为 false 表示是未被回收的垃圾
heap_type对象所在的 ART 堆类型(app、zygote、boot image)
type_id该对象实例所属的类(指向 heap_graph_class)
root_type非空表示该对象是一个 GC 根
root_distance到根的距离(隐藏列)
object_data_id可选的 HPROF 原始字段值与数组数据的 ID(指向 heap_graph_object_data)

heap_graph_reference

说明
reference_set_id引用集合 ID(join key)
owner_id引用持有者对象 id
owned_id被引用对象 id
field_name字段名
field_type_name字段类型名
deobfuscated_field_name反混淆后的字段名

实例查询 1:按类名统计字节数

原文档给出了第一个实用查询——统计每个类名占用的字节数。正如文档所提醒的,该查询直接运行常常得不到可操作的信息,因为 Java 堆中的大部分字节最终落在原始数组(primitive arrays)和字符串上:

select c.name, sum(o.self_size) from heap_graph_object o join heap_graph_class c on (o.type_id = c.id) where reachable = 1 group by 1 order by 2 desc;

结果示意:

namesum(o.self_size)
java.lang.String2770504
long[]1500048
int[]1181164
java.lang.Object[]624812
char[]357720
byte[]350423

实例查询 2:标准库 class_summary_tree 模块

更有价值的做法是利用标准库(stdlib)将堆图归一化为一棵树——总是取到根的最短路径,并计算累计大小,从而看出每类对象到底"留住"了多少内存。原文档给出的用法:

INCLUDE PERFETTO MODULE android.memory.heap_graph.class_summary_tree; SELECT -- The name of the class. name, -- The sum of `self_size` of this node and all descendants of this node. cumulative_size FROM android_heap_graph_class_summary_tree;

结果示意:

namecumulative_size
java.lang.String1431688
java.lang.Class<android.icu.text.Transliterator>1120227
android.icu.text.TransliteratorRegistry1119600
com.android.systemui.statusbar.phone.StatusBarNotificationPresenter$21086209
com.android.systemui.statusbar.phone.StatusBarNotificationPresenter1085593
java.util.Collections$SynchronizedMap1063376
java.util.HashMap1063292

该模块的实际实现位于 src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/class_summary_tree.sql:它先INCLUDE PERFETTO MODULE android.memory.heap_graph.class_tree构建类树,再基于graphs.scan模块的_graph_aggregating_scan做自底向上的累计扫描,把每个节点的self_count/self_size逐层累加为cumulative_count/cumulative_size。输出表android_heap_graph_class_summary_tree的核心列包括:

  • graph_sample_ts:dump 时间戳;
  • upid:进程 id;
  • name:类名;
  • root_type:若该节点是根,描述 Java 根类型,否则为 NULL;
  • self_count/self_size:类名与到根路径均相同的对象个数 / 大小;
  • cumulative_count/cumulative_size:本节点及所有后代节点的self_count/self_size之和。

class_summary_tree外,同目录还提供了更丰富的分析工具链(见 src/trace_processor/perfetto_sql/stdlib/android/memory/heap_graph/):dominator_tree.sql(支配树)、class_tree.sqlobject_tree.sqlbitmap.sqlexcluded_refs.sqlexperimental_flamegraph.sqlheap_graph_class_aggregation.sqlheap_graph_stats.sql等,分别用于支配关系分析、对象级树、位图展示、排除引用、火焰图重建与统计汇总。

底层实现与解析链路

从实现层面看,堆图数据在 trace_processor 中的导入与追踪由 src/trace_processor/importers/art_hprof/art_hprof_parser.cc 与 src/trace_processor/importers/proto/heap_graph_tracker.cc 完成(后者配套的单元测试见 heap_graph_tracker_unittest.cc)。与之相关的还有heap_graph_object_data内部表(HPROF 特有,仅 ART heap dump 填充),存放解码后的字符串内容(value_string)与原始数组数据引用(array_data_idarray_data_hash),这印证了"heap dump 不含对象数据、但 HPROF 路径可选择性保留字符串与数组内容"的设计取舍。

围绕堆图,仓库还预置了若干指标 SQL(见 src/trace_processor/metrics/sql/android/java_heap_class_stats.sql 与 java_heap_histogram.sql),可直接复用于类级统计与堆大小直方图分析。完整的表结构与 stdlib 模块文档可结合 分析文档目录 进一步查阅。

总结

ART Heap Dump 是 Perfetto 针对 Java/Kotlin 堆内存问题提供的高信号量数据源:它只记录引用图、不携带对象数据,因而体积可控且天然适合回答"谁留住了内存"这类问题。在 Android 11+ 上通过android.java_hprof数据源捕获后,既可以在 UI 中借助最短路径与支配树两种火焰图快速定位可疑对象,也可以回到 trace_processor 用heap_graph_*三张表 + stdlib 的class_summary_tree/dominator_tree模块做可复现、可脚本化的深度分析。

【免费下载链接】perfettoProduction-grade client-side tracing, profiling, and analysis for complex software systems.项目地址: https://gitcode.com/GitHub_Trending/pe/perfetto

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询