堆内存检视方案(Native Heap Insight)

Version information

https://about:blank/

Date

Descritpion

More info.

2023-8

适配Android U

Note: /odm下的vendor进程暂不支持

8-10, u-pre MERGED

2023-7

添加对Vendor进程的支持

7-25, t-pre MERGED

2023-4

  1. 引入高性能备用堆分配器,用以隔离系统/业务侧占用和方案自用的内存,对齐meminfo的统计源

  2. 函数调用栈最大深度提升至64帧,显示完整的函数名(Demangled);同时,添加调用栈的最近采样机制(see 4.1)

  3. 输出PSS占用和malloc info.数值,对齐meminfo的输出项 (see 4.2)

  4. 内存统计相关释疑(see 4.3)

  5. 解决少数APP或demo程序无法跟踪的问题

3-28, t-pre MERGED

2022-10

Document creation

查看native堆内存的使用、常规手段一般可以借助meminfo获取native堆对应的memory map和malloc info.信息,体现出PSS占用的数据和堆分配器内部的统计,但却无法直接和业务侧的使用情况相关联。

比如native OOM问题,meminfo仅能给出该业务在问题发生时的内存使用数值,但无法分析该业务的哪部分逻辑在使用,使用了多久、多大、多少次。malloc_debug是当前排查此类问题或查看堆内存使用的一种手段,但它较差的运行时性能、繁琐的分析步骤也一直限制其应用场景和使用范围。

  1. 功能与特点

基于上述痛点和需求,Android T上我们集成了自研的Native Heap Insight工具,用以分析和报告运行时的堆内存使用情况,它的优势体现如下:

较低的性能延迟 -- M2上验证system_server、其malloc平均延迟为 ~5 us;在相同测试维度下相较malloc_debug有数倍以上的提升,此时手机操作、交互无卡顿感。

同时,我们实现了调用栈符号和信息的“一站式”匹配,实时解析后输出;并能够自动识别目标包名、进程,以便启动时即时跟踪,无需借助信号开启。

该工具的整体功能与malloc_debug的调用栈跟踪相近:

  1. 基于malloc/free函数簇的callee hook

  2. 调用栈运行时聚合,并以每个调用栈为单元进行统计:

  3. 输出内存信息:使用时长、申请次数、累计大小

  4. 输出按占用内存大小排序的调用栈TOP-K信息

  5. 输出单次大块内存申请的次数

  6. 支持native进程和Android runtime(APP进程)

  7. 自适应jemalloc和scudo

  8. 零成本接入,支持动态开启

另外,在该方案设计之初,我们就考虑到了研发、测试同学在三方应用稳定性以及内存比对等方面的需要,因此兼容并优化了针对三方APP、特别是头部应用厂商的支持,有相关业务需求的同学也可以尝试该工具。

2. 实现原理简述

基于malloc hook的用户态跟踪方案都大同小异:借助若干“查询表”,跟踪、记录、归并调用栈从而获取想要的信息;安卓malloc_debug、字节Raphael、微信Matrix和我们的Native Heap Insight亦是如此,他们实现细节的差异主要体现在具体业务和需求倾向上的取舍:易用性、集成度、告警机制等等,大体原理示意如下:

  1. 方案接入

https://about:blank/

setprop persist.track.malloc.enable track-heap

setprop persist.track.malloc.program <process or package name[,others]>

注:Vendor进程有所不同,需要在上述属性中添加“vendor.”字样,两者互不冲突,例如:

setprop persist.vendor.track.malloc.enable track-heap

setprop persist.vendor.track.malloc.program vendor.qti.camera.provider-service_64

注:adb shell stop && adb shell start(MIUI 重启)或Native/Vendor进程重启后待其下次启动时生效

支持多个APP或进程同时开启、以“,”间隔,

例如在system server, system ui和语音助手上开启:

setprop persist.track.malloc.program system_server,com.android.systemui,com.miui.voiceassist

例如在三方APP“头条”、B站上开启:

setprop persist.track.malloc.program com.ss.android.article.news,tv.danmaku.bili

运行一段时间后,通过发送特定信号查看目标进程的内存使用情况:

https://about:blank/

pidof <process or package name> | xargs kill -51

信号触发后相关信息将以日志信息输出到logcat:

https://about:blank/

logcat | grep track-heap

  1. 检视日志示例

4.1 调用栈信息

logcat "track-heap"日志会输出按调用栈占用内存大小、从高往低排序的Top-10,参考如下示例:

Note: 这里的调用栈信息、出于性能上的平衡,做了归并处理、只记录一份,即可能有其他的调用栈也符合类似的特征,但这里并未显示。

"sampled at ..."是将最近一次记录的调用栈显示在dump中:

  1. 可以尝试不断地dump以观测调用栈的更新情况 -- 调用栈的内容可能不变,但"sampled"的时间戳会被更新

  2. 如果该时间戳与当前dump时刻相距较远,则说明该调用栈近期未发生内存申请,可以结合自身业务、判断它是否是驻留型内存抑或存在泄漏的可能

4.2 PSS & malloc info.

PSS和malloc info.显示如下:

上述数值是在dump时直接获取,相较meminfo省去了中间的调用链路和临时的内存花销;同时,malloc info.减去了部分、在跟踪之前已被初始化的内存占用,更贴近业务启动后的实际占用情况。

4.3 内存统计释疑

“Native Heap”按照不同的“视角”可以划分如下三层:

  • 从APP的角度,malloc/free函数簇即是上层调用的入口,用以创建和释放虚拟内存块;它体现的是业务实际关心的内存大小,这也是本方案内存统计的层面。

  • 下一层是系统(libc)堆内存分配器,本质是一个虚拟内存管理者。除了向上给业务侧提供所需的内存资源(Allocated bytes),它自身的实现逻辑和数据结构也需要内存消耗,比如:

  • --> Spare space:业务侧分配过但当前已释放的空间,外碎片、预分配等也可划分在这里

  • --> Internal frag.:即内碎片,虽然单个字节数较小但巨大数量下也较为可观

  • --> System use: 分配器内部使用,如缓存、隔离区等

  • 这一层面的统计可以简单地对应到meminfo “Native Heap”一行的后三个字段,或比对 4.2章节 所示的星号“*”标记的字段。

  • 最后是操作系统层面、上层实际需要物理页面(含swap)的内存开销体现在这里。“Pages-to-GC”代表分配器已经确认无需使用、可以释放,但操作系统还未及时回收的空间。这层的统计可以从“Native Heap”一行的前五个字段相对应,也可参考 4.2章节 所示的PSS统计。

  1. 案例分享

K7BP systemui堆内存泄漏

内网统一认证 (Central Authentication Service)

本地复现,初始systemui meminfo:

https://about:blank/

rosemary:/ # dumpsys meminfo com.android.systemui

Applications Memory Usage (in Kilobytes):

Uptime: 1123027 Realtime: 1123027

** MEMINFO in pid 16809 [com.android.systemui] **

Pss Private Private SwapPss Rss Heap Heap Heap

Total Dirty Clean Dirty Total Size Alloc Free

------ ------ ------ ------ ------ ------ ------ ------

Native Heap 54050 52572 1472 31949 54768 104380 96590 7789

运行1小时左右,Native Heap增长到73MB:

https://about:blank/

** MEMINFO in pid 16809 [com.android.systemui] **

Pss Private Private SwapPss Rss Heap Heap Heap

Total Dirty Clean Dirty Total Size Alloc Free

------ ------ ------ ------ ------ ------ ------ ------

Native Heap 73452 71868 1576 31227 74164 132088 120832 11255

此时触发信号打印检视日志如下:

可以看到createRenderInspector()申请的内存总大小排名第一:占用约26MB、累计申请1150次且还在持续增长。这样的申请行为并非业务模块预期,于是结合上述调用栈信息、确认该函数的调用入口:

再找到对应的资源释放的位置、将代码修改如下,内存不再增长,JIRA所述问题得到解决:

End of document

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值