4. dumpsys meminfo详解:内存到底被谁吃了?

做性能优化这么多年,我几乎每天都要跟 dumpsys meminfo

别急,我带你把它掰开揉碎。这一章先吃透指标,再看进程分布,最后聊聊怎么从数据里揪出内存泄漏。

4.1 三个核心指标:PSS、RSS、VSS

这三个缩写,说白了就是系统从不同视角算的内存占用。

指标全称含义特点
VSSVirtual Set Size进程申请的虚拟内存总量包含共享库、已映射文件等,通常很大,参考价值低
RSSResident Set Size进程实际常驻在物理内存中的大小会重复计算共享库,导致总和超过实际物理内存
PSSProportional Set Size按比例分摊共享库后的实际物理内存最接近真实占用,各进程PSS之和≈总内存使用量

我的建议:日常优化只看PSS。RSS容易虚高,VSS更是「虚胖」。我在项目中遇到过一次,某App RSS显示500MB,但PSS只有180MB,结果发现是共享库重复计算导致的——差点冤枉了它。

记住一个原则:排查内存问题,优先看PSS总量;定位泄漏时,再看具体区域(Native Heap、Dalvik Heap等)的PSS变化。

4.2 数据怎么抓?

简单一条命令就能拿到全量信息:

adb shell dumpsys meminfo -a com.example.app

输出结果通常分成几大块:

  • Total PSS by category – 按类别汇总
  • Dalvik Heap – Java堆相关
  • Native Heap – C/C++堆
  • Stack & Code – 栈和代码段
  • Graphics / GL – 图形内存
  • Other dev – 其他设备映射

嗯,这里有个坑要注意——dumpsys meminfo 默认是汇总值,想看详细堆内部分布得加 -a 参数。我刚开始忘了加,盯着总数值看了半天没头绪。

4.3 Native Heap 与 Dalvik Heap 到底怎么分?

Android应用的内存其实分两大阵营:

Dalvik Heap(Java堆)

  • 管理 new 出来的Java对象
  • 由GC自动回收
  • PSS包括对象本身+GC管理开销
  • 常见泄漏:Activity未释放、Bitmap对象、Handler引用

Native Heap(原生堆)

  • 管理 malloc / new 分配的C/C++内存
  • 由开发者手动释放,或通过智能指针
  • 包括:Bitmap像素数据、解码库缓冲区、OpenGL资源
  • 泄漏通常更隐蔽,排查需要用 malloc debug 或 address sanitizer
个人经验:我踩过最深的坑是某视频App的Native Heap持续增长。当时只看Java堆,GC一降下来就觉得稳了。直到用户反馈卡顿、OOM,才发现Native Heap已经吃掉300MB。起因是一个第三方解码库每次解码后没释放内部缓存。从那之后,我习惯把Native Heap列单独画个曲线跟踪。

4.4 各进程内存分布怎么看?

实际工作中,我们经常要对比多个进程的内存占用。 dumpsys meminfo 也支持看所有进程:

adb shell dumpsys meminfo

输出会按PSS从大到小排序。你一眼就能看到哪个进程是「内存大户」。我曾经用它抓过一个系统级预装App,在后台偷偷缓存图片,PSS长期保持150MB+。

输出表格里每行格式类似:

     PID      Pss     Uss     Rss  Name
  12345   180MB   120MB   350MB  com.example.app
  12346    80MB    60MB   120MB  com.android.phone
...

重点关注 Pss 和 Uss(Unique Set Size,独占内存)。如果Uss占比高,说明这个App独立占用了大量物理内存,泄漏风险更大。

4.5 内存泄漏的线索藏在哪儿?

内存泄漏不会直接告诉你说「我泄漏了」,但数据会出卖它。我总结了几条常见特征:

  1. Dalvik Heap 持续增长且GC后不回落 – Java对象泄漏,典型如Activity引用链未断。
  2. Native Heap 增长但Java堆稳定 – C/C++泄漏,常见于图片加载库、编解码器。
  3. Graphics / GL 内存只增不减 – 纹理或Surface未释放,OpenGL相关。
  4. 某个进程的PSS随时间单调递增 – 运行监测3~5次,每次增加10MB以上,基本就是泄漏。
注意:不要把缓存池增长误判为泄漏。我见过一个图片加载库本身会缓存20MB的缩略图,这不算泄漏,是设计意图。判断泄漏要结合「GC后是否释放」「是否随操作次数无限增长」。

4.6 快速定位泄漏的实操流程

分享一下我的标准打法:

  1. 抓基线 – 启动App,等几秒,跑一次 dumpsys meminfo -a,记录各区域PSS。
  2. 执行可疑操作 – 比如反复进出某个Activity、下载大图、播放视频。
  3. 抓第二次 – 操作后等几秒再跑一次,查看变化。
  4. 对比关键区域 – 重点关注 Dalvik Heap 和 Native Heap 的 Pss Total 差值。
  5. 多次采样 – 重复步骤2-4三次以上,如果每次PSS都增加且不回落,基本实锤泄漏。
举个例子:我曾帮一个电商App排查首页卡顿。第一次抓meminfo显示Native Heap 80MB,反复滑10次商品列表后再抓,Native Heap涨到120MB,且没有回落。最终定位是商品图片解码器内部缓存没按策略清理。修复后,Native Heap稳定在85MB左右。
dumpsys meminfo 核心知识体系 VSS 虚拟内存 总申请量,虚高 RSS 常驻内存 含共享库重复 PSS 比例集大小 最真实占用 Dalvik Heap Java对象 + GC管理
泄漏特征:持续增长不回降 Native Heap C/C++分配 + 像素数据
泄漏特征:Java堆稳定却增长
排查路线 1️⃣ 抓基线(dumpsys meminfo -a) 2️⃣ 执行可疑操作 3️⃣ 抓第二次,对比PSS变化 4️⃣ 多次采样 → 确认泄漏

4.7 实战小贴士:怎么用Systrace联合看?

光靠 dumpsys meminfo 还不够,因为它只能拍到快照。真正的卡顿发生在时间线上。我通常这样配合:

  • Systrace 里加内存跟踪 – 开启 -a com.example.app 和 --memtrack,可以看到Java堆和Native堆随时间变化。
  • 在关键操作前后打tag – 比如 Trace.beginSection("enter_detail"),结合meminfo快照,就能定位是哪段代码导致内存飙升。
一个小技巧:我习惯在Systrace中同时勾选 gfx、input、view 和 dalvik。如果某帧掉帧时,Dalvik Heap刚好出现一个陡增,八成是对象分配过多。这时候再切到 dumpsys meminfo 确认具体区域,双管齐下效率很高。

好了,这一章的内容基本讲透。PSS/RSS/VSS的差异、两种堆的特性、以及从数据里找泄漏的套路,都是我在项目里反复验证过的。你拿到一份meminfo输出,至少知道该先看哪个数字,哪里可疑。下回遇到内存问题,不妨先按这套思路走一遍。