Perfetto heapprofd 原理解析 — Native Heap vs ART Heap
一、heapprofd 采集原理总览
heapprofd 是 Perfetto 提供的内存分配分析工具,用于捕获进程的堆内存分配行为。
采集流程:
设备端 Host 端
────── ──────
perfetto (traced)
↓
heapprofd (profilerd)
↓
shared memory (client ↔ heapprofd)
↓
raw-trace 落盘 (/data/misc/perfetto-traces/)
↓
adb pull → 本地
↓
trace_processor_shell 解析
↓
CSV / 报告
关键概念:
- heap_name:heapprofd 针对不同的堆注册名使用不同的采集策略。常见值有
malloc(libc 堆)和com.android.art(ART 虚拟机堆) - dump:heapprofd 定期将共享内存中的数据写入 trace 文件,每次写入称为一次 dump
- callsite:一条唯一的分配调用栈,由调用链上的函数帧组成
二、Native Heap(malloc)数据模型
hook 层:libc malloc/free
heapprofd 通过 hook libc 的 malloc、free、calloc、realloc 等函数,拦截每次内存分配和释放事件。
数据流:增量事件
每次记录的是一条增量事件,size 有正有负:
时间线:
t1: malloc(100) → 记录 {callsite=A, size=+100}
t2: malloc(200) → 记录 {callsite=B, size=+200}
t3: free(100) → 记录 {callsite=A, size=-100}
t4: malloc(150) → 记录 {callsite=C, size=+150}
SUM(size) 的含义
对于某个 callsite:
SUM(size) = +100 + (-100) = 0 → callsite A:已完全释放
SUM(size) = +200 → callsite B:净未释放 200
SUM(size) = +150 → callsite C:净未释放 150
SUM(size) = 净未释放内存(alloc - free),这就是真实的驻留内存。如果 SUM(size) > 0 且持续增长,说明存在内存泄漏。
采样机制
heapprofd 不是记录每一笔分配,而是按采样间隔(-i 参数,单位字节)以概率采样:
-i 4096:分配大小 ≥ 4KB 的更容易被采样到- 被采样到的分配记录实际的 size 和调用栈
- 未被采样到的分配不记录
- 大对象被采样的概率更高,因此采样数据对热点排序仍然可靠
三、ART Heap(com.android.art)数据模型
hook 层:ART GC 堆扫描
ART 虚拟机管理 Java/Kotlin 对象的内存分配和回收。与 malloc/free 不同,ART 使用自己的 GC 机制管理堆内存,没有明确的 free 调用点可以 hook。
heapprofd 对 ART 堆的做法是:定期扫描 ART GC 堆中的存活对象,记录每个 callsite 当前存活对象的 size。
数据流:Dump 快照
每次记录的是某个 dump 时刻的存活快照,所有 size 均为正数:
时间线:
callsite X 分配 100B(t0 创建,t5 被 GC 回收)
callsite Y 分配 200B(t0 创建,一直存活到结束)
dump #1 (t1): 记录 {X: 100}, {Y: 200}
dump #2 (t3): 记录 {X: 100}, {Y: 200}
dump #3 (t7): 记录 {Y: 200} ← X 已被回收,不再出现
SUM(size) 的含义
callsite X SUM(size) = 100 + 100 = 200 (跨 2 次 dump 快照累计)
callsite Y SUM(size) = 200 + 200 + 200 = 600 (跨 3 次 dump 快照累计)
SUM(size) = 跨 dump 快照累计,同一个对象如果跨多个 dump 都存活,会被重复累加。这不是净未释放内存,也不是总共申请的内存量。
为什么 ART 不能用事件模型
| 原因 | 说明 |
|---|---|
| GC 统一管理 | ART 的对象回收由 GC 触发,不是程序员显式调用 free |
| 无明确释放点 | 没有 malloc/free 这样的调用点可以 hook |
| 批量回收 | GC 一次可能回收成千上万个对象,无法逐个记录 free 事件 |
因此 heapprofd 对 ART 堆只能采用快照模式,无法记录分配/释放事件流。
四、两种模式对比
| 对比维度 | Native Heap(malloc) | ART Heap(com.android.art) |
|---|---|---|
| hook 层 | libc malloc/free | ART GC 堆扫描 |
| 数据模型 | 增量事件 | Dump 时刻存活快照 |
| size 符号 | alloc 为正,free 为负 | 全部正数 |
| SUM(size) 含义 | 净未释放(alloc - free) | 跨 dump 快照累计(同一对象重复计算) |
| 峰值 | 峰值 ≈ 总净残留 | 峰值时刻的驻留大小(最有参考价值) |
| 累计值 | 等于真实驻留 | 远大于真实驻留(因为重复计算) |
| 能否判断泄漏 | SUM(size) 持续增长 = 泄漏 | 需对比多次 dump 的驻留值判断 |
| 适用场景 | Native 内存泄漏追踪 | Java/Kotlin 对象分配热点分析 |
五、如何正确使用分析结果
Native Heap
- SUM(size) 就是真实未释放内存,可以直接用于判断是否存在泄漏
- 按 SUM(size) 排序,最大的 callsite 就是最大的泄漏嫌疑
- 采样数据中的 KB 值是真实内存消耗的近似值
ART Heap
ART heap 有三种分析视角,各自用途不同:
1. 跨 dump 快照累计(默认模式)
python3 parse_art_heap.py raw-trace -n 200- SUM(size) 把同一对象在多个 dump 中重复累加,数值膨胀
- 用途:热点排序——哪个 callsite 出现频率高、持续时间长
- 不适合:当作实际内存消耗量
2. 峰值 dump
- 报告自动输出峰值内存出现在第几次 dump
- 用途:了解内存压力最大的瞬间的分布
3. 最后一次 dump 驻留(--last-dump)
python3 parse_art_heap.py raw-trace --last-dump -n 200- 只取最后一次 dump 的存活对象,不跨 dump 累加
- 用途:最终驻留分布——采集结束时还剩多少 Java 堆内存
- 最接近”真实驻留内存”的概念
六、实际案例对比
以 2026-04-17 的 SystemUI ART heap 采集为例(49 次 dump,26.8 秒):
| 分析视角 | 总内存 | callsite 数 | 说明 |
|---|---|---|---|
| 跨 dump 快照累计 | 101.76 MB | 10,361 | 所有 dump 快照值的算术和,数值膨胀 |
| 峰值 dump(第 3 次) | 11.08 MB | — | 内存压力最大的瞬间 |
| 最后一次 dump 驻留 | 4.11 MB | 297 | 采集结束时的真实驻留 |
差异分析:
- 累计 101.76 MB vs 驻留 4.11 MB:说明大部分分配生命周期很短,已被 GC 回收,但因为它们在多个 dump 中出现过,被重复累加导致累计值膨胀
- 峰值 11.08 MB vs 驻留 4.11 MB:说明从峰值到结束期间,约 63% 的驻留对象被 GC 回收
- 驻留 4.11 MB(297 callsite)才是最终真正需要关注的 Java 堆内存
业务模块在不同视角下的差异:
| 模块 | 跨 dump 累计占比 | 最后一次 dump 驻留占比 | 说明 |
|---|---|---|---|
| Handler 消息调度开销 | 52.0% | 62.8% | 持续驻留,生命周期长 |
| 日期时间格式化 (ICU) | 18.9% | 0% | 已完全释放,生命周期短 |
| 动画系统 (Folme) | 14.4% | 28.9% | 持续驻留,生命周期长 |
| 协程/Flow 调度 | 2.3% | 4.2% | 持续驻留 |
| 通知栏/通知渲染 | 4.6% | 0.2% | 大部分已释放 |
ICU 日期时间格式化在累计模式下排第 2(18.9%),但在驻留模式下完全消失,说明这些对象被 GC 及时回收了,不是泄漏嫌疑。真正需要关注的是 Handler.getTraceName 和 Folme 动画——它们在两种模式下都占大头,是持续驻留的分配热点。
本文档配合 parse_art_heap.py 和 parse_heap_profile.py 工具使用