在 Android 系统开发中,为了精准衡量进程的内存消耗,通常会使用 VSS、RSS、PSS、USS 这四个指标。由于内存共享机制的存在,单一的"内存占用"数字往往无法真实反映进程对系统的影响,因此这四个指标提供了不同维度的观察视角。
0x00 核心含义详解
我们可以通过一个简单的模型来理解:假设进程 A 使用了 50MB 私有内存 ,并与进程 B 共享一个 20MB 的动态库。
VSS (Virtual Set Size) - 虚拟耗用内存
- 含义:进程能访问到的全部虚拟地址空间大小。
- 包含:已分配但未实际使用的内存、映射到磁盘的文件、共享库占用的全部空间等。
- 特点 :数值最大。它不代表真实的物理内存消耗,对于性能分析意义较小。
RSS (Resident Set Size) - 实际使用物理内存
- 含义:进程当前实际占用的物理内存(RAM)。
- 包含 :进程的私有内存 + 共享库的全部大小。
- 缺点:存在"重复计算"。如果有 10 个进程都用了那个 20MB 的共享库,每个进程的 RSS 都会计入这 20MB。将所有进程的 RSS 相加,会远大于系统总内存。
PSS (Proportional Set Size) - 比例分配内存
- 含义 :将共享库的大小按共享进程的数量均摊。
- 计算:私有内存 + (共享库大小 / 共享该库的进程数)。
- 优点 :最客观的指标。如果将系统中所有进程的 PSS 相加,结果几乎等于系统实际消耗的总内存。它是衡量进程系统级负担的最佳标准。
USS (Unique Set Size) - 进程独占内存(最常用)
- 含义:进程完全独占的物理内存。
- 包含:只有该进程能访问到的 RAM 区域。
- 优点 :分析内存泄露的核心指标。它表示如果杀死该进程,系统能立刻回收的内存量。
0x01 核心判断标准:为什么 USS 是"金标准"?
在分析泄漏时,我们通常遵循这个优先级:USS > PSS > RSS > VSS。
- VSS 和 RSS 的局限性:由于 RSS 包含了共享库内存,当系统其他进程加载或卸载共享库时,你的进程 RSS 可能会波动,这会产生"伪泄漏"的干扰。
- PSS 的参考意义:PSS 能够反映你的进程对系统总内存的真实贡献。如果 PSS 持续上涨,说明你的应用正在拖慢系统。
- USS 的决定性作用 :USS 只包含进程完全独占的内存。 内存泄漏本质上是进程内部的对象无法被回收,因此内存泄漏一定会直接反映在 USS 的上涨上。
0x02 分析内存泄漏分析的流程
第一步:建立基准线 (Baseline)
在应用启动并进入主界面稳定后,记录当前的内存状态。
shell
adb shell dumpsys meminfo <your_package_name>
重点记录:TOTAL Pss 和 Private Dirty(后者非常接近 USS)。
第二步:执行重复性操作 (Stress Test)
内存泄漏通常发生在 Activity 销毁或长生命周期对象(如 Singleton、Thread)中。执行以下操作:
- 进入一个怀疑有泄漏的页面,再退出。
- 重复上述过程 10 次以上。
- 手动触发几次 GC(在 Android Studio Profiler 中点击小垃圾桶图标,或通过
adb shell cmd activity gc)。
第三步:对比观测
再次运行 adb shell dumpsys meminfo <your_package_name>,观察数据变化:
- 如果 USS (Private Dirty) 阶梯式上升 :说明存在私有内存泄漏(通常是 Java 对象、Bitmap 或 Native 堆内存)。
- 如果 PSS 上升但 USS 稳定:这种情况极少见,可能意味着你调用的系统共享资源(如某些系统服务的缓存)在增加,而不是你进程内部的泄漏。
- 如果 PSS/USS 均不增加,但 VSS 疯狂增长:说明你在不断地申请虚拟地址空间(例如创建了大量线程,每个线程占用 1MB 虚拟栈空间,但实际还没开始使用物理内存)。
shell
Applications Memory Usage (in Kilobytes):
Uptime: 102275 Realtime: 102275
** MEMINFO in pid 3069 [com.android.launcher3] **
Pss Private Private Swap Rss Heap Heap Heap
Total Dirty Clean Dirty Total Size Alloc Free
------ ------ ------ ------ ------ ------ ------ ------
Native Heap 11128 11060 0 0 13796 19812 13511 2600
Dalvik Heap 2316 2188 0 0 6620 27729 3153 24576
Dalvik Other 2587 2264 0 0 3528
Stack 736 736 0 0 740
Ashmem 2 0 0 0 8
Gfx dev 1244 1244 0 0 1244
Other dev 36 0 36 0 284
.so mmap 6076 212 128 0 54512
.jar mmap 1922 0 0 0 26648
.apk mmap 14824 0 13900 0 38756
.ttf mmap 54 0 0 0 228
.dex mmap 213 4 0 0 504
.oat mmap 55 0 0 0 2044
.art mmap 7794 7404 4 0 20040
Other mmap 1179 4 32 0 4984
EGL mtrack 40288 40288 0 0 40288
GL mtrack 6104 6104 0 0 6104
Unknown 466 448 0 0 1100
TOTAL 97024 71956 14100 0 97024 47541 16664 27176
App Summary
Pss(KB) Rss(KB)
------ ------
Java Heap: 9596 26660
Native Heap: 11060 13796
Code: 14256 123424
Stack: 736 740
Graphics: 47636 47636
Private Other: 2772
System: 10968
Unknown: 9172
TOTAL PSS: 97024 TOTAL RSS: 221428 TOTAL SWAP (KB): 0
...
0x03 针对不同类型的泄漏分析
A. Java 层泄漏 (Activity/Fragment)
-
现象 :
TOTAL Pss和Java Heap持续增长。 -
分析 :查看
dumpsys meminfo最后的Objects部分。Views和Activities的数量。如果你退出了页面,Activities数量没有减少,那就是典型的 Activity 泄漏。
B. Native 层泄漏 (C++/JNI)
- 现象 :
Native Heap指标持续增长,且USS同步上涨。 - 分析 :如果你在 JNI 中使用了
malloc或new但没有free,这部分内存会体现在Native Heap中。
C. 共享内存/文件描述符泄漏 (SharedMemory/FD)
如果 SharedMemory 没有被正确关闭:
-
现象 :
USS可能不会显著增长(如果你已经munmap了),但文件描述符 (FD) 会耗尽。 -
检查方式:
bashadb shell ls -l /proc/<pid>/fd | wc -l例如检查
3069号进程(com.android.launcher3)shelladb root adb shell ls -l /proc/3069/fd | wc -l # 输出 # 82重复操作后,如果 FD 数量不断增加,说明你的
SharedMemory或ParcelFileDescriptor没有执行close()。
0x04 自动化检测工具推荐
虽然手动分析 dumpsys 很有帮助,但在实际开发中,建议配合以下工具:
| 工具 | 优势 |
|---|---|
| LeakCanary | 自动化程度最高,直接在手机端弹窗告知 Java 泄漏引用链。 |
| Android Studio Profiler | 可视化实时查看 PSS 分布,支持 Capture Heap Dump 分析堆快照。 |
| Perfetto / Systrace | 适合分析 Native 层和系统级的内存分配行为。 |