一条命令三步定位内存泄漏:Perfetto heapprofd 实战完整指南
2026/9/24 23:34:05 网站建设 项目流程

一条命令三步定位内存泄漏:Perfetto heapprofd 实战完整指南

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

你有没有遇到过这种情况:应用在用户手里用半小时越来越卡,最后被系统杀掉,而你自己的测试机上完全复现不出来?内存类崩溃是 Android 线上最顽固的问题之一——系统内存吃紧时,lmkd 会按 oom_score_adj 从高到低杀掉缓存中的应用,而一个每次操作只泄漏 2MB 的缺陷,30 次操作后就能吃掉 60MB,足以让你的 App 成为第一批被杀的对象。Perfetto 的 heapprofd(采样式 native 内存剖析器)可以在真机上拦截 malloc/free 调用,把每一笔未释放的内存归因到具体调用栈,采样间隔默认 4096 字节,对前台应用几乎无感。本文用 3 条命令,带你走完"录制—读图—修复验证"的全流程。

一个复现不出来的泄漏:商品列表越滑越吃内存

先讲一个典型的排查现场。某资讯类 App 收到用户反馈:连续下滑信息流 20 分钟后应用被杀,重启后继续。开发环境用 LeakCanary 跑了 30 分钟,Java 堆没有增长,查无实据。问题在于:LeakCanary 只看 Java 对象引用,而内存增长的元凶是 native 侧——dumpsys meminfo显示每次滚动 10 次,Native Heap 的 Private Dirty 稳定上涨约 2MB,Java 堆纹丝不动。

排查分三步走:

  1. 确认增长曲线:用 Perfetto 录制 30 秒内存计数器,滚动列表,观察mem.rss.anon(进程匿名内存,即真正的物理占用)是否单调上升;
  2. 归因到调用栈:用 heapprofd 录制同一段操作,看火焰图里哪条调用路径的"未释放内存"在涨;
  3. 定位代码:火焰图里锁定到离屏渲染缩略图的 EGL 路径——每个缩略图新建了渲染上下文却从未销毁,1.6MB 的纹理缓冲跟着上下文一起被挂住。

这个案例的关键启示:Java 堆干净不代表没有泄漏,native 分配(哪怕你一行 C++ 都没写,JNI 背后的框架 API 也会走 malloc)必须单独查。

录制第一条 native 内存 profile:最简 heap_profile 命令

前置条件:设备 Android 10+,已连接 ADB;在 user 版本系统上,目标 App 的 manifest 需要带<profileable android:shell="true"/>或 debuggable 标记,否则 profile 会是空的。仓库里没有现成脚本的话,先 clone 一份:

git clone https://gitcode.com/GitHub_Trending/pe/perfetto cd perfetto

下面这条命令录制 30 秒(-d单位是毫秒)的 native 堆 profile,每 5 秒出一个快照(-c 5000),结束后在/tmp/heap_profile-latest下生成 raw-trace:

tools/heap_profile android -n com.example.news -d 30000 -c 5000

录制期间反复执行你的复现路径(滚动、切换页面),然后 Ctrl-C 结束。输出里会出现Wrote profiles to /tmp/... (symlink /tmp/heap_profile-latest),把它指向的raw-trace文件拖进 Perfetto UI 的网页前端即可查看。

图1:Perfetto UI 录制页的 Native heap profiling 配置项:目标进程名、采样间隔(默认 4096B)、连续快照间隔

⚠️新手最大的误区:以为 heapprofd 能看到"之前"的分配。它是非回溯式的——只统计开始录制之后发生的 malloc/free,启动时就已经存在的内存它一概不知。所以如果你的问题是"进程现在为什么这么大",heapprofd 回答不了;只有当泄漏会随时间持续发生时(绝大多数泄漏都是),才能靠观察增量抓住它。另一个隐蔽的坑:多个录制会话指向同一个进程时只有第一个生效,如果 profile 为空,先adb shell killall perfetto清掉残留会话再重试。

读懂火焰图:未释放内存的 4 个度量维度

打开 raw-trace,点时间轴上菱形标记的 "Native heap profile" 切片,默认展示 Unreleased Malloc Size 火焰图:横轴是宽度(大小),顶层是main()/pthread_start等入口,越往下越接近最终的malloc调用点。根节点标注的总 MiB 就是本次记录期内"分配了但没 free 掉"的总量。

图2:Native heap profile 火焰图:默认按 Unreleased Malloc Size 聚合未释放内存,根节点显示 46.31 MiB 总量

四个度量模式各查各的病(火焰图左上角可切换):

  • Unreleased Malloc Size(默认):未释放字节数,查泄漏主战场;
  • Unreleased Malloc Count:未释放分配次数,专抓"单个很小、数量巨大"的碎片泄漏;
  • Total Malloc Size:含已释放的总分配量,查分配热点(churn),比如频繁创建销毁的临时对象;
  • Total Malloc Count:总分配次数,看 allocator 压力。

开了-c连续快照后,时间轴上会排开多个切片,每个切片对应 5 秒窗口;选中相邻的多个切片还能做窗口合并对比,泄漏路径会随滑动次数明显变宽:

图3:开启 -c 5000 连续快照后,时间轴上按时间窗排列的多个 Native heap profile 切片

图形看不爽就上 SQL。Perfetto 把堆数据存进标准表,UI 的 Query (SQL) 标签页或命令行trace_processor query都能跑:

tools/trace_processor query /tmp/heap_profile-latest/raw-trace \ "INCLUDE PERFETTO MODULE android.memory.heap_profile.summary_tree; SELECT name, self_size, cumulative_size FROM android_heap_profile_summary_tree WHERE cumulative_size > 1048576 ORDER BY cumulative_size DESC LIMIT 10;"

self_size是该帧作为叶子时的未释放字节数,cumulative_size是出现在任意层级的未释放字节数——排序后 Top 10 基本就是修复优先级表。

图4:UI 的 Query (SQL) 窗口:上半区写 PerfettoSQL,下半区看结果表

案例闭环:两轮录制拿到修复前后的对比数据

回到信息流案例。修复前的量化基线(30 秒滚动,5 个连续快照):

  • Native Heap PSS:48MB → 110MB,净增 62MB;
  • 火焰图eglCreateContext路径 cumulative_size 约 60.9MB,且每个快照切片比上一个宽 2MB 左右;
  • mem.rss.anon计数器单调上行,无回落。

修复动作:把 EGL 上下文收敛为进程内单例复用,销毁缩略图时同步释放纹理。修复后用完全相同的命令和复现路径再录一次(这是闭环的关键——变量只有一个,就是代码):

tools/heap_profile android -n com.example.news -d 30000 -c 5000

对比结果:同样 30 秒滚动,Native Heap PSS 48MB → 51.8MB,净增 3.8MB(其中 2.9MB 是缓存正常水位),eglCreateContext路径 cumulative_size 从 60.9MB 降到 4.2MB,降幅 93%;mem.rss.anon曲线在滑动停止后回落到基线附近。

图5:mem.rss.anon 等内存计数器时间线:RSS 尖峰与用户操作时间点一一对应,是验证泄漏是否复发的直观依据

工具选型:Java 堆、native 堆、内核内存各查哪份数据

Perfetto 的内存数据源不止 heapprofd,选错工具会白忙一场。按"你想回答什么问题"对号入座:

你要回答的问题数据源最低版本触发方式
native 内存去哪了 / 谁在泄漏(malloc 级)heapprofd native profileAndroid 10tools/heap_profile android
Java/Kotlin 对象被谁引用着ART heap dumpAndroid 11tools/java_heap_dump -n <进程名>
Java 对象分配太频繁(churn)ART allocation profilingAndroid 12heap_profile--heaps com.android.art
进程 RSS/swap 随时间的走势内存 ftrace 计数器内核 tracepointftracekmem/rss_stat等事件
为什么被 lmkd 杀了oom_score_adj/lowmemory_kill内核 tracepointftrace 事件 +linux.process_stats
绕过 malloc 的直接 mmap 大块syscall 事件 / perf 全采样Android 14 / 12syscall_events: "sys_mmap"或 perfperiod: 1

两点补充:Java 堆 dump 是整张对象引用图的一次性快照(支持回溯,但看不到分配调用点),和 heapprofd 正好互补,细节见 Java 堆剖析文档 与 内存案例手册。文件映射内存(dex、so)用adb shell showmap <PID>更直接,Perfetto 管不了这部分。

图6:ART heap dump 在 UI 中的火焰图视图:按最短可达路径聚合 Java 对象占用,可切换支配树视角

进阶:自定义分配器、采样权衡与后台场景

  • 自定义分配器对 heapprofd 不可见。如果你的 App 有自己的内存池(图片缓存、解码器 buffer 常见),用 heapprofd Custom Allocator API 把池子注册进来,分配/释放时各报一笔,就能进火焰图:
#include "heap_profile.h" static uint32_t g_heap = AHeapProfile_registerHeap( AHeapInfo_create("example.image_cache")); void* my_malloc(size_t size) { void* ptr = real_malloc(size); AHeapProfile_reportAllocation(g_heap, (uintptr_t)ptr, size); return ptr; // 释放侧对应调用 AHeapProfile_reportFree }
  • 采样间隔是精度和开销的旋钮-i默认 4096,即平均每分配 4KiB 记一笔;前台交互型 App 保持默认即可,后台长时录制可以调到 8192 减半开销。大于间隔的大块分配不走采样,按真实大小记录,所以大泄漏不会因为采样被稀释。
  • 后台与低端设备场景:录制时 App 退后台,mem.rss.anon会告诉你它是否被 ZRAM 换出;被 lmkd 杀掉的进程在lowmemory_kill事件里有时间戳,可以和泄漏曲线对出"死亡时刻"。这些配置都基于 ftrace,见 memory 案例手册。

上线前内存健康自检清单

把下面这些项塞进你的发布流程,泄漏就不用等到线上:

录制前

  • user 构建的 App 已打profileable android:shell="true"标记
  • 复现路径写成可重复操作的脚本化步骤(滚动 30 次 / 切换 10 个页面)
  • adb shell killall perfetto排除残留会话

录制中

  • 连续快照间隔 ≤ 操作周期,至少 5 个切片用于看趋势
  • 前台敏感场景未调低采样间隔导致数据稀疏

读图时

  • Unreleased 和 Total 两个维度都看过(区分泄漏 vs churn)
  • 火焰图里对libart.so加了 Hide Frame 过滤,减少噪音
  • SQL 里用cumulative_size排过 Top 10,和图上看到的最大块对得上

修复后

  • 用同一命令、同一复现路径重录,对比 Native Heap 增量(要求降幅 ≥90% 或给出解释)
  • 低内存设备(4GB RAM)上跑通完整复现路径,mem.rss.anon无单调爬升

最后几条实践建议

  1. 先定基线再谈优化:没有修复前 30 秒的量化录制,任何"优化后内存降了"都不可信。
  2. native 和 Java 两条线并行查dumpsys meminfo的 Private Dirty 分属两类堆,LeakCanary 只管得住后者。
  3. 把 heap_profile 接进 CItrace_processor query是无界面输出的,泄漏回归可以做成一条卡发布门槛的自动化。
  4. 火焰图配合代码读:最大的块往往在公共路径上,真正要修的是它下面第二、第三深的叶子帧。

💡专业提示:把-c 5000的连续快照作为日常巡检标配,每次发版前跑一次并存档 raw-trace;用cumulative_sizeTop 10 做版本间 diff,能在用户报障前两周就抓到缓慢增长的新泄漏,修复成本比线上救火低一个数量级。更多 SQL 写法见 PerfettoSQL 入门,命令行细节见 trace_processor 参考 与 heap_profile 参考。

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

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

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

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

立即咨询