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 Dump | Native 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_id | 与heap_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;结果示意:
| name | sum(o.self_size) |
|---|---|
| java.lang.String | 2770504 |
| 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;结果示意:
| name | cumulative_size |
|---|---|
| java.lang.String | 1431688 |
| java.lang.Class<android.icu.text.Transliterator> | 1120227 |
| android.icu.text.TransliteratorRegistry | 1119600 |
| com.android.systemui.statusbar.phone.StatusBarNotificationPresenter$2 | 1086209 |
| com.android.systemui.statusbar.phone.StatusBarNotificationPresenter | 1085593 |
| java.util.Collections$SynchronizedMap | 1063376 |
| java.util.HashMap | 1063292 |
该模块的实际实现位于 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.sql、object_tree.sql、bitmap.sql、excluded_refs.sql、experimental_flamegraph.sql、heap_graph_class_aggregation.sql、heap_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_id、array_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),仅供参考