EN

Android Perfetto 系列 14:用 heapprofd 找 native 分配调用栈

Word count: 5.5kReading time: 23 min
2026/05/04
loading

内存问题最麻烦的地方在于:数字涨了,不代表马上能知道谁在涨。dumpsys meminfo 能告诉你 RSS/PSS,Java heap dump 能告诉你对象保留关系,但 JNI/C++、Skia/Bitmap 的 CPU 侧分配、播放器里的 native wrapper 这些问题,经常还需要知道“哪个调用栈发起了 malloc/new”。

第 14 篇讲 heapprofd。它的价值是把 native allocation、free 和调用栈放进 Perfetto trace,再和业务事件、线程调度、Binder 放到同一个时间轴里看。

heapprofd 适合放在趋势确认之后。先用 RSS/PSS、meminfo 或线上监控确认趋势,再用 heapprofd 找 malloc/new 调用栈;这篇不解决 Java 对象引用和图形 backing 内存。

下文的顺序是:先说什么时候上 heapprofd、它和 Java heap dump、Java allocation sampling 的边界在哪里;接着抓一份最小可用的 native heap profile,并把它和系统 trace 合在一起抓,便于和 Binder、调度一起看;UI 里怎么看 Heap Profile,SQL 怎么查未释放分配;采样间隔怎么理解才不会被误读;符号化的可信度问题;以及发布报告前的检查清单,最后把 RSS/PSS 内存趋势对应到具体的调用栈上。

内存问题要分几张账

开始抓 heapprofd 前,先把内存数据分成几类。很多误判都来自把它们混在一起。

数据 常见来源 回答什么问题
进程 RSS / swap 趋势 linux.process_stats 目标进程 RSS、anon/file RSS、swap、oom_score_adj 有没有变化
进程 PSS / Private Dirty dumpsys meminfo/proc/<pid>/smaps_rollupprocess_stats_config.scan_smaps_rollup PSS、Private Dirty、SwapPss 是否上涨;scan_smaps_rollup 受权限和目标进程限制
系统内存压力 linux.sys_stats MemAvailable、Cached、ZRAM、vmstat、PSI 是否说明系统在回收或承压
native allocation 调用栈 android.heapprofd 哪些 malloc/new 调用栈贡献了分配,哪些还没释放
Java 对象保留关系 android.java_hprof、MAT、Android Studio Profiler 哪些 Java 对象还被谁持有

heapprofd 默认统计的是目标进程向 libc malloc/free、new/delete 这类 allocator 请求的字节数。它不等于 RSS,也不覆盖所有 native backing。

GraphicBuffer、dma-buf、GPU texture、驱动侧分配、直接 mmap/memfd/ashmem 或绕过 libc malloc 的 allocator 不在默认 malloc 账里。基于 malloc 的 arena 可能只看到 arena 大块,看不到内部对象。图形内存要先看 meminfo Graphics/GL、dmabuf、GPU/graphics 计数、SurfaceFlinger / BufferQueue 关系。

heapprofd 只补充 App/HWUI/Skia/JNI 路径上的 malloc/new 证据。

先用这张表决定下一步:

现象 优先工具 heapprofd 角色
Java heap 上涨,对象保留明显 HPROF / MAT / Android Studio 不作为第一入口
Native Heap / malloc wrapper 上涨 heapprofd 找 malloc/new 调用栈
Graphics / GL / dmabuf / Surface buffer 上涨 meminfo、dmabuf、GPU/graphics、SurfaceFlinger 只用于排除 App/native wrapper 是否也在分配
RSS/PSS 上涨但 heapprofd 不高 smaps_rollup、maps、mmap/syscall、dmabuf/graphics 先拆账,不要直接判 heapprofd 抓错
PSS 不涨但 RSS 涨 smaps / file mapping / allocator cache 先不要下泄漏结论

allocator 线程缓存、碎片、ZRAM、page 粒度都会让 RSS 和 heapprofd 对不上。看到 RSS 高而 heapprofd 不高时,要先考虑这些边界,不要立刻判成 heapprofd 抓错了。

反过来也一样:heapprofd 里某条调用栈累计分配很高,不代表进程 RSS 会同幅度上涨。它可能是分配 churn:频繁申请、很快释放,Total malloc size 高,但 Unreleased malloc size 不高。内存问题要先分清“占住不放”和“反复制造临时对象”。

怀疑 mmap/native heap 之外的大块分配时,改查 smaps 的 Private_Dirty/Rss/Pss、maps 区间、Android 14+ syscall sys_mmap/sys_munmap/sys_madvise,或者 perf/simpleperf 的 mmap syscall callstack。heapprofd 只用于确认 malloc wrapper 是否也在贡献。

什么时候用 heapprofd

Java heap dump 看不到增长来源时,先看增长落在哪张账。只有 native heap 或 native wrapper 明显可疑时,再进入 heapprofd。

heapprofd 适合这些问题:

  • Native Heap / malloc wrapper 持续上涨,但 Java heap dump 看不出明显对象保留。
  • Bitmap、Skia、播放器 buffer、JNI/C++ 模块怀疑有 libc malloc/new 路径上的 native backing 占用。
  • C++ 模块、native service、daemon 的 malloc/new 异常。
  • 想知道某次业务操作后未释放分配是否增加。
  • 想把内存增长和线程运行、Binder、业务 Track Event 关联起来。

这些问题不该优先用 heapprofd:

  • 只想看 Java 对象引用关系。用 Java heap dump。
  • 想把每一次小分配都精确记成账本。heapprofd 默认是采样 profiler。
  • 目标 App 在 user build 上没有 debuggableprofileable。这类进程通常不能被 shell profile。
  • GraphicBuffer、dma-buf、GPU driver memory、Surface buffer 数量上涨。先走 meminfo Graphics/GL、dmabuf、GPU/graphics、SurfaceFlinger 的证据链,heapprofd 只用来排除或定位 malloc/new 包装层。

目标资格也要分开看。user build 主要面向 debuggable/profileable Java apps;userdebug/eng 可抓大多数 App 和 system services,但少量 critical services 可能被 SELinux never_profile_heap 之类策略禁止。system process 不靠 <profileable> 解锁。

快速抓一份 native heap profile

官方更推荐先用 tools/heap_profile 脚本。它会处理很多设备侧细节,适合单进程快速定位。

这条命令按进程名抓 15 秒,采样间隔 4096 bytes,每 5 秒产生一个 continuous dump:

1
2
3
4
5
6
tools/heap_profile android \
-n com.example.app \
-d 15000 \
-i 4096 \
-c 5000 \
-o /tmp/heapprofd-com-example-app

输出目录里会有 raw-trace 和转换后的 profile 文件。把 raw-trace 拖到 Perfetto UI,就能看到 Heap Profile diamond。

按进程名抓和按 PID 抓差别很大。-n com.example.app 会命中已经运行的同名进程,也会等待后续启动的新进程;目标从 zygote specialize 到 App 进程时,heapprofd 可以尽早接上。多进程 App 要分别写多个 -n / process_cmdline,例如 com.example.appcom.example.app:remotecom.example.app:player;否则 remote/native 子进程的 malloc 不会进入 profile。按 PID 抓更适合已经稳定运行的现场,但会错过启动早期分配。官方也提醒,哪怕按进程名抓,zygote specialize 最早的一小段分配仍可能没记到。

如果要在 user build 上用 adb / shell 抓 App,需要 manifest 里允许 shell profiling:

1
2
3
<application ...>
<profileable android:shell="true" />
</application>

这条路径只解决本地 shell profiling 权限,不等于线上可以远程抓任意用户进程。

Android 15 API 35 起,App 可以通过 ProfilingManager 请求 heap profile;Android 16 的 Profiling module 面向 public devices field profiling,但结果会脱敏、只包含请求进程相关信息,并且受系统限流,不保证每次请求都会满足。线上方案要把它和 adb/profileable 的实验室路径分开设计。

和系统 trace 合在一起抓

脚本适合快速定位,手写 TraceConfig 适合把 heapprofd 和系统数据源合并。先看最小 heapprofd 配置:android.heapprofdprocess_cmdline、采样间隔、continuous dump、shared memory 和阻塞策略。

系统上下文是增强项:process_stats 看 RSS/PSS 趋势,sys_stats 看系统内存压力,ftrace 关联线程和时间窗,track_event 关联业务动作。allocation-heavy 场景里,heapprofd + ftrace 会互相增加扰动;heap+system-context preset 只适合短窗口实验室复现,并且要记录 ftrace/heapprofd stats、目标场景耗时是否被 profiler 改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
duration_ms: 15000

buffers { size_kb: 131072 fill_policy: DISCARD } # 0: heapprofd
buffers { size_kb: 32768 fill_policy: RING_BUFFER } # 1: system context

data_sources {
config {
name: "linux.process_stats"
target_buffer: 1
process_stats_config {
scan_all_processes_on_start: true
proc_stats_poll_ms: 1000
}
}
}

data_sources {
config {
name: "linux.sys_stats"
target_buffer: 1
sys_stats_config {
meminfo_period_ms: 1000
meminfo_counters: MEMINFO_MEM_AVAILABLE
meminfo_counters: MEMINFO_CACHED
meminfo_counters: MEMINFO_SWAP_FREE
meminfo_counters: MEMINFO_ZRAM
vmstat_period_ms: 1000
vmstat_counters: VMSTAT_PGFAULT
vmstat_counters: VMSTAT_PGMAJFAULT
vmstat_counters: VMSTAT_OOM_KILL
}
}
}

data_sources {
config {
name: "linux.ftrace"
target_buffer: 1
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_waking"
atrace_categories: "am"
atrace_categories: "view"
atrace_apps: "com.example.app"
}
}
}

data_sources {
config {
name: "track_event"
target_buffer: 1
track_event_config {
enabled_categories: "memory"
enabled_categories: "app"
}
}
producer_name_filter: "com.example.app"
}

data_sources {
config {
name: "android.heapprofd"
target_buffer: 0
heapprofd_config {
sampling_interval_bytes: 4096
process_cmdline: "com.example.app"
continuous_dump_config {
dump_phase_ms: 5000
dump_interval_ms: 5000
}
shmem_size_bytes: 8388608
block_client: false
# Android 11+ / matching Perfetto builds only.
max_heapprofd_memory_kb: 262144
max_heapprofd_cpu_secs: 30
}
}
}

这仍然是 native heap 模板,不是图形内存模板。排查 GraphicBuffer、dma-buf、GPU texture、Surface buffer 时,要额外对照 dmabuf、GPU/graphics 计数、dumpsys meminfo Graphics/GL、SurfaceFlinger layer / buffer 信息;heapprofd 只能回答 malloc/new 侧有没有对应贡献。

heapprofd buffer 使用 DISCARD 时,buffer 必须足够大。heap profile 常在 dump 点集中写入,DISCARD 满后会丢新数据,对末尾 diamond 和未释放大头很危险。抓完必须检查 traced_buf_chunks_discardedheapprofd_* stats;长 Trace 或写入量不确定时,优先给 heapprofd 独立大 buffer,必要时改 RING 并在报告里说明旧 dump 可能被覆盖。

临时排查时,直接把 PBTX 通过 stdin 传给 perfetto 最稳。输出路径在 Android 上放到 /data/misc/perfetto-traces/

1
2
3
adb push config.pbtx /data/local/tmp/config.pbtx
adb shell 'cat /data/local/tmp/config.pbtx | perfetto -c - --txt -o /data/misc/perfetto-traces/heap.pftrace'
adb pull /data/misc/perfetto-traces/heap.pftrace .

手写 HeapprofdConfigtools/heap_profile 的默认行为不完全一样。block_client 可以在 heapprofd shared memory 满时阻塞目标进程,换取数据完整性;脚本的 --block-client 是默认行为,但手写配置需要自己决定。

实验室 leak 复现可以设 block_client: true 并配 block_client_timeout_us,用目标进程 malloc 路径的阻塞换完整性;交互性能分析不要默认开,因为它可能反向影响 jank/latency。交互场景通常用 block_client: false,再通过更高采样间隔、更大 shmem_size_bytes 和 stats 检查接受不完整风险:必须检查 heapprofd_buffer_overranheapprofd_non_finalized_profileheapprofd_last_profile_timestamp 是否覆盖目标窗口。

把 heapprofd 放进系统 trace 时,建议同时保留少量业务 marker。App 内用 android.os.Trace / AndroidX Trace 会落到 atrace;Perfetto SDK Track Event 需要单独启用 track_event data source。

比如播放器场景至少要有 operation_startsteady_stateoperation_endrelease_done/gc_idle_done 四类 marker。没有 release marker 时,结论只能写“操作后保留增加”,不能写“泄漏”。android.log 数据源只支持 userdebug;user build 上不要把 logcat 当成这份配置默认能抓到的证据。

UI 怎么看 Heap Profile

导入 trace 后,在目标进程下找 Heap Profile 轨道。轨道上的 diamond 代表一次 dump。点击 diamond 后,底部会展示 flamegraph。

continuous dump 的每个 diamond 都是从记录开始到该时间点的累计结果。先看未释放量判断占住不放,再看总分配量判断 churn。读图时按这个顺序走:

  1. 看连续几个 diamond 的 Unreleased malloc size 是否持续增加。
  2. Total malloc size,判断是不是分配 churn 很高但释放也快。
  3. 用 Left Heavy 视图找最大的调用栈。
  4. 先找 allocator / ART glue 之上的第一个 App、SDK 或业务帧。
  5. 回到时间轴,关联触发操作、业务 Track Event、线程运行。

不要用单个 diamond 判泄漏。泄漏判断至少要看“操作前、操作后、回收后”几个点。如果操作后上涨,回收后仍然不降,再看未释放调用栈。

实验室里可以用 adb shell killall -USR1 heapprofd 额外触发一次 snapshot。比如“进入页面前、页面稳定后、退出页面并完成 release/close/析构后”各触发一次,比只靠固定 5 秒间隔更容易把业务动作和 diamond 对上。

只有 native backing 由 Java wrapper、NativeAllocationRegistry 或 Cleaner/finalizer 持有时,才把等待 idle、finalizer、GC 放进这个状态点。allocator cache 和碎片还可能导致 heapprofd 下降,但 RSS 不立刻下降。缓存不是“不释放”的委婉说法,必须有容量上限、命中/复用证据、退出或压力下回收策略;否则只能写“疑似长期保留”。

SQL 怎么查未释放分配

heapprofd 会写入调用栈相关表:

含义
heap_profile_allocation allocation/free 的样本和大小
stack_profile_callsite 调用栈节点
stack_profile_frame 函数帧
stack_profile_mapping so、apk、jar、binary 映射
stack_profile_symbol 离线符号化结果

单目标 profile 快速读图时,优先用官方标准库里的 summary tree。它会沿着 stack_profile_callsite.parent_id 聚合整条调用栈,结果更接近 flamegraph 里的 cumulative view:

1
2
3
4
5
6
7
8
9
10
INCLUDE PERFETTO MODULE android.memory.heap_profile.summary_tree;

SELECT
name,
mapping_name AS map_name,
cumulative_size / 1024.0 / 1024.0 AS unreleased_mb
FROM android_heap_profile_summary_tree
WHERE cumulative_size > 0
ORDER BY cumulative_size DESC
LIMIT 50;

summary tree 适合单目标 profile 的快速读图。它没有进程列,不要直接用于多进程回归统计。多进程 App、:remote 进程或同名进程重启时,要回到原始表按 upid、PID、进程名和 diamond 时间点过滤,避免把别的进程归到目标进程。

如果要按进程、diamond 时间点和 leaf frame 做粗查,再回到原始表。这条 SQL 只看末尾一个 dump 时间点,重点看 SUM(a.size):allocation 是正数,free 是负数,求和后更接近该 diamond 上的 Unreleased malloc size。它不用于最终归因,最终报告必须回到 summary tree 或 flamegraph 取完整调用栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
WITH target_process AS (
SELECT upid, pid, name, cmdline
FROM process
WHERE name = 'com.example.app'
ORDER BY start_ts DESC
LIMIT 1
),
target_dump AS (
SELECT MAX(ts) AS dump_ts
FROM heap_profile_allocation
WHERE upid = (SELECT upid FROM target_process)
)
SELECT
a.ts AS dump_ts,
p.upid,
p.pid,
p.name AS process_name,
p.cmdline AS process_cmdline,
a.callsite_id,
f.name AS frame_name,
m.name AS mapping_name,
SUM(a.size) / 1024.0 / 1024.0 AS leaf_unreleased_mb,
SUM(a.count) AS unreleased_count
FROM heap_profile_allocation a
JOIN target_process p USING (upid)
JOIN target_dump d ON a.ts = d.dump_ts
JOIN stack_profile_callsite c ON a.callsite_id = c.id
JOIN stack_profile_frame f ON c.frame_id = f.id
JOIN stack_profile_mapping m ON f.mapping = m.id
GROUP BY a.ts, p.upid, p.pid, p.name, p.cmdline, a.callsite_id, f.name, m.name
HAVING leaf_unreleased_mb > 0
ORDER BY dump_ts DESC, leaf_unreleased_mb DESC
LIMIT 50;

SQL 只能帮你快速找大头。原始表查询把调用栈拆成了 frame 行,容易只看到 mallocrealloc 这类 leaf frame;summary tree 和 UI flamegraph 更适合读完整归因。写稳定脚本前,先用 PRAGMA table_info(heap_profile_allocation); 确认字段,避免不同 Perfetto 版本导致查询失效。

泄漏判断要做三点对比:操作前、操作后、释放后。这段是报告查询骨架,baseline_dump_tsafter_action_dump_tsafter_release_dump_ts 可以来自业务 marker 最近的 heap dump:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
WITH target_process AS (
SELECT upid, pid, name
FROM process
WHERE name = 'com.example.app'
ORDER BY start_ts DESC
LIMIT 1
),
selected_dumps(label, dump_ts) AS (
VALUES
('baseline', 100000000000),
('after_action', 120000000000),
('after_release', 150000000000)
),
alloc_by_dump AS (
SELECT
d.label,
a.callsite_id,
SUM(a.size) / 1024.0 / 1024.0 AS unreleased_mb,
SUM(a.count) AS unreleased_count
FROM heap_profile_allocation a
JOIN selected_dumps d ON a.ts = d.dump_ts
WHERE a.upid = (SELECT upid FROM target_process)
GROUP BY d.label, a.callsite_id
),
pivot AS (
SELECT
callsite_id,
SUM(CASE WHEN label = 'baseline' THEN unreleased_mb ELSE 0 END) AS baseline_unreleased_mb,
SUM(CASE WHEN label = 'after_action' THEN unreleased_mb ELSE 0 END) AS after_action_unreleased_mb,
SUM(CASE WHEN label = 'after_release' THEN unreleased_mb ELSE 0 END) AS after_release_unreleased_mb
FROM alloc_by_dump
GROUP BY callsite_id
)
SELECT
callsite_id,
baseline_unreleased_mb,
after_action_unreleased_mb,
after_release_unreleased_mb,
after_action_unreleased_mb - baseline_unreleased_mb AS action_delta_mb,
after_release_unreleased_mb - baseline_unreleased_mb AS retained_delta_mb,
after_action_unreleased_mb - after_release_unreleased_mb AS released_delta_mb,
CASE
WHEN after_release_unreleased_mb - baseline_unreleased_mb > 1 THEN 'leak_candidate'
WHEN after_action_unreleased_mb > baseline_unreleased_mb
AND after_release_unreleased_mb <= baseline_unreleased_mb THEN 'peak'
ELSE 'unknown'
END AS conclusion_type
FROM pivot
ORDER BY retained_delta_mb DESC
LIMIT 50;

释放后仍保留才偏泄漏;操作中升高但释放后回落是峰值;长期保留但有容量上限、复用证据和压力回收策略,才可能写成缓存。

报告字段可以固定成这一组:

1
2
trace_name,process_name,upid,pid,scenario,dump_phase,dump_ts_ms,sampling_interval_bytes,dump_interval_ms,top_stack_frame,mapping_name,unreleased_mb,retained_delta_mb,total_malloc_mb,allocation_count,symbol_status,profileable_state,data_loss_status,evidence_grade,conclusion_type,next_action
heap-run01,com.example.app,42,1234,player_open,after_release,150000,4096,5000,PlayerBuffer::Alloc,libplayer.so,8.4,6.9,42.1,128,strong,profileable,clean,strong,leak_candidate,inspect_owner_module

采样间隔怎么理解

heapprofd 不是默认记录每一次分配。sampling_interval_bytes: 4096 的含义是:平均每分配 4096 bytes 采一个样本,再按采样概率归因到调用栈。足够大的 allocation 可能按真实大小记录,但不要把采样间隔理解成“低于它就估算,高于它就精确”的硬分界;不同 Android 版本和 heap 配置还会影响实际行为。

怎么选:

  • 4096 bytes 是常用起点,适合大多数本地分析。
  • 怀疑大量小对象 native 分配时,可以降低间隔,但开销会上升。
  • 分配速率太高导致 buffer overrun 时,先增大 shared memory 或提高采样间隔。
  • 手写 HeapprofdConfig 必须显式设置非 0 的采样间隔;想接近精确可以设 1,但开销很高。0 是无效配置,Android 12 之前还可能让目标进程崩溃。

报告里不要把采样结果写成“精确分配了 12.34MB”。更合适的写法是:“在 4096 bytes 采样间隔下,未释放分配主要集中在 A/B/C 三条调用栈,A 调用栈占比最高”。采样 profiler 的价值是找方向和排序,不是替代 allocator 账本。

小对象密集、样本少、窗口短、调用栈分散时,top stack 排名会不稳定。before/after 比较必须保持同一采样间隔、相近负载和同一目标窗口;不要比较 1% 或 2% 这类小差异,除非多次复现。报告里的 evidence_grade 可以按这个规则写:样本充足、连续 dump 趋势一致、符号完整为 strong;命中 data loss、profile incomplete、unknown symbols、样本少或只复现一次为 weakweak 时写“候选调用栈”,不要写“根因调用栈”。

Java heap dump 和 Java allocation sampling

这部分是对开头几张内存账的补充:heapprofd 能看 native malloc/new,也能在 Android 12 或更高版本上切到 Java allocation sampling,但它仍不替代 Java heap dump。

能力 输出 适合问题
heapprofd native profiling malloc/new 调用栈 谁分配了 native 内存
Java heap dump 对象保留图 谁持有 Java 对象
Java allocation sampling Java 创建对象的调用栈 哪些代码制造了大量 Java allocation churn

Java allocation sampling 可以通过 heapprofd 配置 heaps: "com.android.art" 打开,也可以用脚本参数 --heaps com.android.art。它需要 Android 12 或更高版本。它记录对象创建时的调用栈,不记录对象什么时候被 GC,也不等价于 Java heap dump 的保留关系。

符号化和可信度

如果 flamegraph 里都是地址、unknown 或混淆后的 Java/Kotlin 名字,先处理符号化和反混淆。官方推荐对 collected trace 运行 traceconv bundle 生成带符号信息的归档,再按符号化文档补 native symbols 和 ProGuard/R8 mapping。

报告里要记录 APK/build id、native symbols 版本、ProGuard/R8 mapping 版本;Build ID mismatch 时降级为地址级证据。第三方 SDK 或 stripped so 只剩地址时,下一步是补对应 build 的 unstripped symbols / mapping,而不是直接改业务代码。

读调用栈时也不要盲目过滤 libart.so、allocator glue 或系统库。ART 帧有时正是 Java 调 native、NativeAllocationRegistry 或对象创建路径的关键上下文。

先找 allocator/ART glue 之上的第一个 App、SDK 或业务帧;只有确认某类帧只是重复胶水层时再过滤。看到 DEDUPED、ICF 造成的函数折叠、Build ID mismatch、只有一帧等情况,要先按符号化问题处理。

unwind 可信度也要看:未知帧比例、单帧栈比例、Build ID 是否匹配、Java frame 是否可用、目标 ABI / Android 版本 known issues。缺符号会改变归因可信度;报告里要区分“已符号化可归因”和“只能定位到 so / allocator glue”。

报告里要写清楚三件事:

  • 目标进程是否 profileable/debuggable。
  • sampling_interval_bytes 和 continuous dump 间隔。
  • stats 里是否出现 heapprofd buffer overrun、client error、profile incomplete、heapprofd_unwind_time_usheapprofd_client_spinlock_blockedheapprofd_sampling_interval_adjusted 或 data loss。

出现 data loss 或 heapprofd buffer overrun 时,不要把数字写成精确账本。更合适的说法是“该调用栈是未释放分配的大头,采样条件如下”。

heapprofd 专用 health check 要单独跑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SELECT name, idx, severity, source, value
FROM stats
WHERE value != 0
AND (
name GLOB 'heapprofd_*'
OR name GLOB 'stackprofile_*'
OR name GLOB 'symbolization_*'
OR name IN (
'symbolization_tmp_build_id_not_found',
'heapprofd_buffer_overran',
'heapprofd_buffer_corrupted',
'heapprofd_client_error',
'heapprofd_missing_packet',
'heapprofd_non_finalized_profile',
'heapprofd_rejected_concurrent',
'heapprofd_hit_guardrail',
'heapprofd_sampling_interval_adjusted',
'heapprofd_client_spinlock_blocked'
)
)
ORDER BY name, idx;

发布报告前检查

采集条件:

  • heapprofd 需要 Android 10 或更高版本;Java allocation sampling 需要 Android 12 或更高版本。
  • user build 上用 adb / shell 抓目标 App,通常需要 debuggable<profileable android:shell="true"/>
  • 按 PID 抓运行中进程可能错过启动早期分配;按进程名等待启动能覆盖更多启动阶段。
  • runtime profiling 不是立刻生效,目标进程很空闲时,下一波分配前可能还没启用。
  • 同一个目标进程同时只能被一个相关 session profile;冲突时先确认是否有旧 perfetto session。

数据可信度:

  • 32-bit 程序在部分旧 Android 版本上有限制,遇到空 profile 要回到官方 Known Issues 查版本差异。
  • heapprofd stats、stackprofile stats、symbolization stats 必须过一遍;命中 overrun、non-finalized、unknown symbols 时降级。
  • heapprofd 数字和 RSS 对不上很常见,碎片、线程缓存、allocator cache、ZRAM 都会造成差异。

结论边界:

  • Total malloc size 高不等于泄漏,先和 Unreleased malloc size、连续 diamond、业务操作时间点一起看。
  • 图形 backing 高不等于 heapprofd 一定高。Graphics/GL、dmabuf、GPU、Surface buffer 要单独成账。
  • 把结论归类为 leak / peak / cache / churn / out-of-scope graphics memory / evidence insufficient,每类对应不同下一步动作。

把内存趋势落到调用栈

heapprofd 解决的是 native allocation 调用栈问题。它不替代 Java heap dump,也不替代 RSS/PSS 趋势;它补上的是“谁分配了 libc malloc/new 管辖的 native heap 内存”。GraphicBuffer、dma-buf、GPU texture 和 Surface buffer 要走另一张图形内存账,heapprofd 只能辅助看 App/native wrapper 是否也在制造 malloc/new 压力。

实战里可以按这个顺序走:

  1. 用 process stats、meminfo、smaps 或线上监控确认内存趋势。
  2. 如果增长落在 Native Heap / malloc wrapper / heapprofd 可见 allocator 路径,进入 heapprofd;如果只是 RSS/PSS 涨,先用 smaps_rollup、meminfo 分类、mmap/syscall 或 dmabuf/graphics 证据拆账。
  3. 在 UI 里看 Unreleased malloc size 和 flamegraph。
  4. 用 SQL 找大头,再回到 flamegraph 看完整调用栈。
  5. 补符号化、写清采样条件和可信度。
  6. 把结论归类为 leak、peak、cache、churn、图形内存账外问题或证据不足。

参考文档

  1. Memory: Callstack-based Allocation Profiling
  2. Memory: Java heap dumps
  3. Memory counters and events
  4. heapprofd design
  5. TraceConfig reference
  6. Track Events
  7. Android Log data source
  8. ProfilingManager
  9. Android Profiling module

关于我 && 博客

欢迎关注 Android Performance

CATALOG
  1. 1. 内存问题要分几张账
  2. 2. 什么时候用 heapprofd
  3. 3. 快速抓一份 native heap profile
  4. 4. 和系统 trace 合在一起抓
  5. 5. UI 怎么看 Heap Profile
  6. 6. SQL 怎么查未释放分配
  7. 7. 采样间隔怎么理解
  8. 8. Java heap dump 和 Java allocation sampling
  9. 9. 符号化和可信度
  10. 10. 发布报告前检查
  11. 11. 把内存趋势落到调用栈
  12. 12. 参考文档
  13. 13. 关于我 && 博客