鸿蒙应用匿名页内存泄漏排查指南
前言
你有没有遇到过这样的场景:App 明明没有做什么”大动作”,内存占用却像吹气球一样越胀越大,最后系统直接”出手”,前台闪退,用户一脸懵圈?别急,这很可能不是 App 在”故意捣乱”,而是匿名页内存泄漏在背后”悄悄作祟”。
匿名页内存——听起来像个冷门概念,但它其实是 RSS 内存泄漏的二级根因之一。简单来说,它就是那些没有对应到磁盘上任何具体文件的内存页,一旦被大量申请却迟迟不释放,就会像漏水的龙头一样,把系统可用内存一点点”滴”光。
本文将带你从运维态和开发态两个视角,一步步拆解匿名映射过大导致内存泄漏的故障模式,并结合真实案例,教你如何用 DevEco Studio 的 Profile r 工具精准定位问题。无论你是刚入坑的开发者,还是经验丰富的老兵,这篇文章都能帮你少走弯路。
一、匿名页内存:内存世界的“隐形租客”
在开始排查之前,我们先来认识一下这位“隐形租客”——匿名页内存。
想象一下,你的应用进程就像一个房东,管理着一座大楼(物理内存)。有些房间(内存页)里住的是有“房产证”的租客——它们对应着磁盘上的具体文件,系统随时可以查户口;而另一些房间里的租客则是“黑户”,没有房产证,也没有对应的磁盘文件,这些就是匿名页内存。
匿名页内存通常通过 mmap 等系统调用申请,用于存放堆数据、栈数据、共享内存等。正常情况下,这些内存用完后会被及时释放,租客搬走,房间恢复空闲。但问题出在——如果租客赖着不走呢?
💡 匿名页内存泄漏:应用进程通过 mmap 等方式大量申请无文件后备的私有内存,导致物理内存占用过高。这类内存无法回收、不关联磁盘,常因泄漏或缓存膨胀引发内存紧张,挤占系统可用内存,可能会触发系统主动管控,造成应用前台闪退等体验影响。
通常情况下,匿名页内存泄漏是因为应用使用 mmap 申请了内存,但忘记释放。这就好像你借了一笔钱,用完却不还——时间一长,债主(系统)可不干了。
二、给内存“贴标签”:prctl() 的妙用
面对大量的匿名页内存,如何快速找到“问题租客”?一个实用的技巧是在 mmap 阶段就给内存绑定名字,就像给每个租客发一张“身份证”,排查时直接按名字找人。
具体做法是使用 prctl() 方法进行命名:
// C++ 代码片段
// 使用 prctl 为匿名内存区域设置调试名称
// 参数说明:
// PR_SET_VMA - 操作类型:设置 VMA(虚拟内存区域)属性
// PR_SET_VMA_ANON_NAME - 子操作:设置匿名内存名称
// addr - 内存区域的起始地址
// size - 内存区域的大小(字节)
// "MyDebugBuffer" - 自定义的内存名称,便于后续排查时识别
prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, addr, size, "MyDebugBuffer");
这样一来,在后续的 smaps 日志中,你就能直接看到 “MyDebugBuffer” 这个名字,而不是千篇一律的 [anon],排查效率直接双倍。
三、运维态排查:从日志中“破案”
3.1 从 smaps 日志入手
当你的 App 在用户设备上出现内存问题时,第一手线索往往来自运维日志。开发者可以根据 smaps 日志分析方法,对维测日志中的轻量化 smaps 维测信息进行分析。
关键判断标准:如果类型为 “AnonPage other” 的内存占比最大,那么就可以将当前遇到的 RSS 内存泄漏问题精确到——匿名映射过大导致的内存泄漏。
smaps 维测的关键字为:
// 日志关键字 代码片段
// smaps 维测日志关键字
LOGGER_MEMCHECK_SAMPLIFY_SMAPS_INFO:
3.2 内存栈日志分析
如果要进一步分析泄漏点,可以参考内存栈日志获取方法,获取内存栈日志后,将日志导入 DevEco Studio,按照内存栈日志分析方法对调用栈进行分析。
💡 提示:应用内存栈受到应用内存泄漏的速度影响较大。对于快速泄漏问题,推荐参考订阅资源泄漏事件(ArkTS)方法,在订阅资源泄漏事件的同时,通过 configEventPolicy() 方法补充订阅应用页面切换信息,以此分析泄漏场景,并在开发态对泄漏问题进行复现和定位。
四、开发态排查:用 Profiler “抓现行”
如果说运维态排查是“事后机灵”,那开发态排查就是“当场捕获”。当应用发生了 RSS 内存泄漏时,可以参考开发态问题分析方法,结合场景复现并使用 DevEco Studio 中 Profiler 工具的 Allocation 功能,定位匿名页内存泄漏问题。
具体操作路径:在 Profiler 的 Memory 面板中,选择 AnonPage Other 选项,观察 AnonPage Other 内存的增长趋势——哪条曲线冲得最高,哪里就是问题所在。
五、真实案例:一次 mmap 引发的“内存灾难”
5.1 案例背景
以下为负向案例:应用通过 mmap 申请大量内存,但未及时释放,最终导致 RSS 内存泄漏,触发系统管控,出现前台闪退。本案例旨在指导开发者遇到匿名页内存泄漏时,应如何进行分析。
5.2 运维态分析过程
第一步:获取故障日志
通过订阅资源泄漏事件,开发者可以在沙箱中接收到故障日志,路径为 :
# 故障日志路径(XXXX为动态生成的时间戳)
/data/storage/el2/log/resourcelimit/RESOURCE_OVERLIMIT_XXXX_XXX.log
在故障日志中找到关键字 LOGGER_MEMCHECK_SAMPLIFY_SMAPS_INFO,读取数据如下:
// 日志数据 代码片段
// smaps 维测日志输出示例
// 列说明:Size(申请大小) | Rss(居住集) | Swap(交换分区) | Category(类别) | Counts(数量) | Name(名称)
LOGGER_MEMCHECK_SAMPLIFY_SMAPS_INFO:
Size Rss Swap Category Counts Name
512 0 52 arkts-static heap 2 [anon:ArkTs Static Object Space]
256 0 224 arkts-static heap 1 [anon:ArkTs Static Object Space]
8869848 4218636 2827804 AnonPage other 48 [anon]
1778944 1080 1052 ark ts heap 29 [anon:ArkTS Heap]
512 12 4 ark ts heap 2 [anon:ArkTS Heapread only space]
3072 2376 192 ark ts heap 12 [anon:ArkTS Heapnon movable space]
256 24 24 ark ts heap 1 [anon:ArkTS Heapshared non movable space]
256 28 20 ark ts heap 1 [anon:ArkTS Heapshared read only space]
40960 8 40472 ark ts heap 1 [anon:ArkTS Heapshared huge object space]
5888 5144 584 ark ts heap 23 [anon:ArkTS Heapappspawn space]
1792 460 1204 ark ts heap 7 [anon:ArkTS Heapsemi space]
768 108 540 ark ts heap 3 [anon:ArkTS Heapshared old space]
2560 228 1364 ark ts heap 10 [anon:ArkTS Heapshared appspawn space]
33554432 0 120 AnonPage other 5 [anon:partition_alloc]
// ... 更多日志行 ...
第二步:按 Category 聚类分析
分析 smaps 维测数据,按照 Category 列进行聚类,对每种类型的 Rss 列、Swap 列求和,得到每种内存类型的申请总量。经过排序,发现 AnonPage other 类型且 Name 为 [anon] 的内存总量最大:
// 聚类分析结果 代码片段
// 聚类分析结果:AnonPage other 类型内存总量最大
// 48 个匿名内存区域,合计申请约 8.5GB,居住集约 4.2GB
Size Rss Swap Category Counts Name
8869848 4218636 2827804 AnonPage other 48 [anon]
......
根据上述信息,可初步定界当前 RSS 内存泄漏故障为其他类型匿名页内存申请过大。
第三步:导入 DevEco Studio 分析调用栈
• 打开 DevEco Studio 中的 Profiler 组件,单击下图 ① 处导入获取的内存栈日志。
• 单击选择 All Anonymous VM 下的 VM:others 泳道,如下图 ② 处。
• 单击 ③ 处 Call Trees 查看内存申请调用栈。
• 单击 ④ 处选择 Created & Existing,筛选申请并且未释放的内存及其调用栈。
• 找到内存申请异常的内存及其调用栈,如下图 ⑤ 处框选的内容。

图1:DevEco Studio Profiler 内存栈分析界面
分析内存栈指向的代码段,可以得出:单次点击 mmap 按钮,会通过 Native 层 mmap_memory() 函数,申请一次超大匿名页内存且未释放。
5.3 问题代码分析
ArkTs 层函数:响应按钮点击动作,调用 Native 层的 LEAK_MEMORY_MMAP() 函数

图2:ArkTs 层代码——响应按钮点击,调用 Native 层函数
// ArkTs 代码片段
// ArkTs 层代码:响应按钮点击事件
// 每次点击按钮,都会调用 Native 层的 LEAK_MEMORY_MMAP() 函数
// 问题:每次调用都会申请新的匿名页内存,但没有对应的释放逻辑
button.onClick(() => {
// 调用 Native 层函数申请内存
LEAK_MEMORY_MMAP();
// 注意:此处没有对应的内存释放调用
});
Native 层函数实现:每次执行都会通过 mmap() 创建匿名页内存,并且退出前未释放

图3:Native 层代码——mmap 申请内存但未释放
// C++ 代码片段
// Native 层代码:mmap 申请匿名页内存
// 问题:每次调用 mmap() 申请内存后,没有调用 munmap() 释放
void* mmap_memory() {
// 申请一块匿名页内存(无文件后备)
// MAP_ANONYMOUS 表示不关联任何文件
// MAP_PRIVATE 表示私有映射,修改不会写回文件
void* ptr = mmap(NULL,
size, // 申请大小
PROT_READ | PROT_WRITE, // 读写权限
MAP_PRIVATE | MAP_ANONYMOUS, // 私有匿名映射
-1, // 文件描述符(匿名映射时为 -1)
0); // 文件偏移量
// 问题所在:函数返回后,ptr 指向的内存从未被 munmap() 释放
return ptr;
}
// 正确的做法:使用完后往调用 munmap(ptr, size) 释放内存
5.4 开发态复现与定位
对于开发态存在的问题,开发者大致能够推断出当前出现 RSS 内存泄漏的测试场景。通过尝试复现此场景,并使用 DevEco Studio 中 Profiler 工具的 Allocation 功能抓取内存异常增长的点,可以参考基础内存:Allocation 分析。
• 完成录制后,在 Memory 的 options 按钮中选择 AnonPage Other,观察 AnonPage Other 内存增长趋势,发现 AnonPage Other 内存的增长趋势最明显。

图4:Memory Profiler 中 AnonPage Other 内存增长趋势
• 选择 ① 处 ALL Anonymous VM 中的 VM:others 泳道如 ② 处,单击 ③ 处 Call Trees 查看内存申请调用栈,单击 ④ 处筛选 Created & Existing 可以找到内存增长点的内存申请调用栈。

图5:Call Trees 中定位内存申请调用栈
根据调用栈能够找到具体的代码行。通过分析代码功能,发现每次点击按钮mmap申请内存使用不合理_同步,ArkTs 层会响应点击动作,并调用 Native 层的 LEAK_MEMORY_MMAP() 函数,通过 mmap 申请 500MB 匿名页内存。连续点击多次按钮后,最终发生了 RSS 内存泄漏。
ArkTs 层函数:响应按钮点击动作,调用Native层的LEAK_MEMORY_MMAP()函数

图6:ArkTs 层代码(开发态复现)
Native 层函数:每次执行都会通过mmap创建匿名页内存,并且推出前未释放

图7:Native 层代码(开发态复现)
六、预防建议:给内存管理上把锁
说到底,匿名页内存泄漏的根因就一句话:mmap() 和 munmap() 没有成对出现。但这“一句话”背后,蕴着不少坑。
以下是几条实用的预防建议:
• 全面理清代码执行流:涵盖异常跳转、循环迭代与异步信号上下文,严格保证 mmap() 与 munmap() 成对出现,阻断内存泄漏。
• 善用 prctl() 命名机制:在 mmap 阶段就为内存区域绑定调试名称,排查时一目了然。
• 建立内存使用台账:对大块匿名页内存的申请和释放做日志记录,便于跟踪生命周期。
• 定期使用 Profiler 巡检:不要等到用户投诉才排查,开发阶段就养成定期用 Allocation 功能检查内存趋势的习惯。
• 代码审查加入内存检查项:在 Code Review 流程中,将 mmap/munmap 成对检查列为必审项。
七、写在最后
匿名页内存泄漏就像家里的水管漏水——刚开始只是滴响滴响,不以为意;等到水漫金山时,再想补救就晚了。通过本文介绍的运维态和开发态两种排查思路,配合 DevEco Studio 的 Profiler 工具,开发者可以像侦探一样,从 smaps 日志的蛛丝马迹中锁定泄漏点,再从调用栈中找出“首罪犯”。
记住:内存管理不是“用完再说”,而是“借必有还”。每一次 mmap() 都应当有对应的 munmap() ,就像每一次借书都应当还一样——这是程序员的基本素养,也是 App 稳定运行的底线。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
如果你也在匿名页内存泄漏的问题上踏过坑,欢迎在评论区分享你的排查经验。
技术之路,我们一起少走坑。
🔗 官网开发者学堂视频:https://developer.huawei.com/consumer/cn/training/result?type2List=201783644516849879&orderBy=1&courseType=5

🔗 社区DFX专题文章: https://developer.huawei.com/consumer/cn/forum/subject/2101218731402391001


【扫码加入 HarmonyOS DFX 技术交流群】
本文由 @华为开发者联盟 授权发布于人人都是产品经理。未经作者许可,禁止转载
题图来自Unsplash,基于CC0协议
该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务
- 目前还没评论,等你发挥!

起点课堂会员权益




