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 的 mallocfreecallocrealloc 等函数,拦截每次内存分配和释放事件。

数据流:增量事件

每次记录的是一条增量事件,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/freeART 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 MB10,361所有 dump 快照值的算术和,数值膨胀
峰值 dump(第 3 次)11.08 MB内存压力最大的瞬间
最后一次 dump 驻留4.11 MB297采集结束时的真实驻留

差异分析:

  • 累计 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.getTraceNameFolme 动画——它们在两种模式下都占大头,是持续驻留的分配热点。


本文档配合 parse_art_heap.py 和 parse_heap_profile.py 工具使用