第4章:观察内存趋势图——PSS、Java Heap、Native Heap、堆增长模式
现象识别:内存趋势图中的危险信号
在 Android Profiler 的 Memory 面板中,正常的应用内存会呈现“锯齿形波动”——分配内存后回收,峰值稳定。当出现以下三种模式时,高度疑似内存泄漏:
- 持续上升型:PSS 或堆大小随操作次数线性递增,从不回落。
- 阶梯型:每次操作后内存上升一级,GC 后不完全下降,形成台阶。
- 锯齿不回缩型:频繁 GC 导致频繁锯齿,但锯齿底部逐渐抬高。
❗ 易混淆场景:单次大对象分配后触发 GC,内存陡升陡降属正常。危险信号是“底部不回缩”或“斜率持续为正”。
数据采集:在 Profiler 中锁定三个关键指标
点击 Memory Profiler 中的录制按钮,选择 “Record allocation” 以同时捕获堆栈。重点关注以下三个指标:
| 指标 | 含义 | 采集技巧 |
|---|---|---|
| PSS (Proportional Set Size) | 应用实际占用的物理内存(按比例分配共享库) | 观察整体趋势,若持续增长说明存在泄漏 |
| Java Heap | Dalvik/ART 堆中的 Java 对象占用量 | 逐次触发操作后 dump,对比 Java Heap 变化 |
| Native Heap | 由 C/C++ 代码通过 malloc/new 分配的内存 | 若 Java Heap 稳定但 Native Heap 上涨,排查 JNI 泄漏 |
# 命令行辅助采集(可选)
adb shell dumpsys meminfo <包名> | grep -E "PSS|Java Heap|Native Heap"
采集节奏:重复执行 5~10 次关键操作(如页面切换、图片加载),每次操作后停顿 2 秒让 GC 完成,观察趋势是否回落。
原因定位:基于趋势图的故障树与判断规则
使用以下故障树快速收缩泄漏范围:
- PSS 持续上升 + Java Heap 同步上升 → Java 堆内存在泄漏(Activity/Fragment 未释放、静态集合类持有 Context)。
- PSS 持续上升 + Java Heap 稳定 → 排查 Native Heap 或匿名共享内存(Bitmap 未 recycle、WebView 泄漏、JNI 全局引用未删除)。
- PSS 或堆在 GC 后底部抬高 → 泄漏对象被 GC Root 持有,每次操作产生新对象无法回收。
# 判断规则口诀
Java堆涨 → 查引用
Native堆涨 → 查 JNI / Bitmap / WebView
PSS涨但堆图稳 → 查 mmap / 线程 / 匿名页
修复动作:针对趋势图特征的精准修复
- Java Heap 阶梯上升 → 使用
LeakCanary或MAT分析 hprof,定位被 GC Root 引用的泄漏对象(常见:Runnable、View.OnClickListener匿名内部类持有 Activity)。 - Native Heap 持续增长 → 检查
BitmapFactory是否调用recycle()(Android 8.0+ 需手动释放 native 内存);检查 JNI 全局引用是否及时DeleteGlobalRef。 - PSS 异常但堆图正常 → 排查线程创建数量、内存映射文件(
mmap)、WebView 渲染进程泄漏(单独进程需主动销毁)。
案例:某电商 App 在商品详情页反复切换后,PSS 从 80MB 升至 280MB。趋势图显示 Java Heap 稳定在 40MB,Native Heap 从 20MB 升至 120MB。定位为
WebView 每次创建新 WebViewClient 未及时 destroy(),修复后趋势图稳定在 100MB。
复测:验证修复效果
- 重新录制内存趋势,重复 10 次操作。
- 确认 PSS 在 GC 后能回落至基线水平(浮动不超过 5%)。
- 检查 Java Heap 和 Native Heap 不再出现“底部抬高”或“线性上升”。
- 使用
adb shell dumpsys meminfo对比修复前后峰值。
预防:建立内存趋势基线
- 在 CI 中集成 Memory Profiler 自动录制,每次 UI 测试后导出 trend 报告。
- 设定 PSS 和 Heap 阈值警报,例如 Java Heap 峰谷差超过 20% 即标记。
- 代码审查时要求对 Bitmap、Handler、WebView 等资源使用时添加显式释放注释。
工具建议:使用
simpleperf record + meminfo 脚本实现趋势图自动化分析;配合 LeakCanary 2.x 自动捕获泄漏堆栈。