1 认识内存泄漏:定义、危害、常见场景、与内存溢出的区别
1.1 现象识别 —— 用户与开发者感知的信号
内存泄漏在运行时并不会直接抛出异常,而是通过一系列逐渐恶化的性能现象暴露出来。在排查流程的第一步,需要准确识别这些“早期预警信号”。
| 现象类别 | 具体表现 | 判断规则 |
|---|---|---|
| UI 卡顿 / 掉帧 | 操作界面时频繁出现丢帧(>16ms),尤其页面切换后仍然持续 | 使用 Profile GPU Rendering 检测垂直同步红线 |
| GC 频繁 / 停顿长 | Logcat 出现大量 Background concurrent copying GC freed,且间隔 < 5s | adb logcat -v time | grep "GC" 观察单次 GC 耗时 > 10ms |
| 内存持续增长不回落 | 在 Activity 返回桌面后,用 dumpsys meminfo 观察 Java Heap 未下降 | 反复进入/退出同一页面,每轮堆增量累加 > 1MB |
| OOM 崩溃(低频但致命) | 应用突然闪退,Logcat 报告 OutOfMemoryError 或 java.lang.OutOfMemoryError | 分析 tombstone / hprof 确定堆溢出处 |
📌 排查提示: 若应用仅在高频操作下出现一次 OOM,优先怀疑巨图加载或临时对象爆炸而非泄漏;反复访问同一页面后 Heap 稳步上升,才是泄漏的典型信号。
1.2 数据采集 —— 用工具锁定泄漏现场
确认存在内存异常后,执行标准的数据采集流程,为后续定位提供原始证据。
- 抓取堆转储(Heap Dump): Android Studio → Profiler → Memory → Dump Java heap。
- 获取内存快照命令行:
adb shell am dumpheap com.example.app /sdcard/dump.hprof
adb pull /sdcard/dump.hprof
- 记录系统级内存摘要:
adb shell dumpsys meminfo com.example.app -d
** MEMINFO in pid 12345 [com.example.app] **
Pss Private Private SwapPss Rss Heap Heap Heap
Total Dirty Clean Dirty Total Size Alloc Free
------ ------ ------ ------ ------ ------ ------ ------
Native Heap 10240 10240 0 0 10240 20480 15360 5120
Dalvik Heap 5120 5120 0 0 5120 8192 6144 2048
....
采集黄金规则: 每次采集前先执行一次
System.gc()(通过 Profiler 触发)以减少弱/软引用干扰;同一场景至少采集 3 次,取堆增长趋势而非单次值。
1.3 原因定位 —— 内存泄漏的本质与判断
结合 hprof 分析的引用链,可以定位到导致泄漏的“根引用”。下图展示了从分配、残存到崩溃的完整泄漏路径。
1.4 修复动作 —— 典型泄漏场景与修复方案
下表列出实际项目中最常见的 5 类内存泄漏场景,并给出对应的修复动作。
| 场景 | 泄漏原因 | 修复动作 |
|---|---|---|
| 静态变量持有 Activity | static Context context = this; | 改为 Application Context;或使用弱引用 WeakReference |
| 匿名内部类(Handler / Runnable) | Handler 持有外部 Activity 引用,而 MessageQueue 未清除 | 使用静态内部类 + WeakReference;Activity onDestroy 时调用 handler.removeCallbacksAndMessages(null) |
| 单例模式持有了 View / Context | Singleton 内部缓存了 Activity 级别的对象 | 单例只依赖 Application Context;若必须缓存,注册生命周期回调及时释放 |
| 未取消的观察者 / 监听器 | 注册了 EventBus / SensorManager 等,忘记反注册 | 在 Activity/Fragment onDestroy 中执行 unregister() |
| Bitmap 对象未及时 recycle | 大量 Bitmap 驻留在 Native Heap 且未释放 | 使用 BitmapFactory.Options.inBitmap 复用;或确保系统 GC 能回收(避免全局引用) |
⚠️ 修复陷阱: 不要盲目将 Activity 实例传给 Application。修复前务必使用 hprof 确认引用链,避免引入新的泄漏或空指针。
1.5 复测 —— 验证修复是否生效
- 重复执行引发泄漏的操作流程(至少 3 轮进入/退出)。
- 使用 Profiler Memory 记录每次操作后的堆大小,观察是否有稳定的回落。
- 用
adb shell dumpsys meminfo对比修复前后的 Pss Total 差值。 - 执行 Monkey 测试(1000 随机事件)后再次 dump hprof,检查泄漏对象是否仍存在。
通过标准: 同一场景下重复操作 5 次后,堆内存增量 < 512KB,且无新增 GC Root 指向已销毁的组件实例。
1.6 预防 —— 从源头减少泄漏风险
- 编码规范: 禁止在非 Application 类中使用
static修饰 Context/View;内部类优先使用静态 + WeakReference。 - 工具化检查: 在 CI 流程中集成
LeakCanary自动化检测,每次构建都运行泄漏测试。 - 自动化回归: 编写 Espresso 用例覆盖高频操作路径,并在测试结束时 assert
LeakCanary无报告。 - 基线监控: 通过性能监控 SDK(如 Matrix)上报内存峰值,触发阈值时自动采集 hprof 归档。
1.7 与内存溢出的区别 —— 判断规则
| 对比维度 | 内存泄漏 (Memory Leak) | 内存溢出 (OutOfMemory) |
|---|---|---|
| 本质 | 对象无法被 GC 回收,占用了不应占用的堆空间 | 堆已满,无法再分配新对象 |
| 表现形式 | 堆大小随时间阶梯式增长,但一般不立即崩溃 | 突然抛出 OutOfMemoryError 导致进程死亡 |
| 因果关系 | 累积泄漏最终会引发 OOM | OOM 可由一次巨大的对象分配或长期泄漏造成 |
| 定位手段 | Heap Dump 分析引用链 | 分析 hprof 中最大对象,或检查单个分配栈(如大 Bitmap) |
| 典型修复 | 断开不应有的引用 | 减少单次分配、复用对象、降低最大堆限制需求 |
📐 判断规则口诀:
内存泄漏 → 堆持续涨不回落;内存溢出 → 一次分配爆缸。
泄漏是慢病,溢出是绝症。泄漏不一定会立刻溢出,但溢出一定包含泄漏(或瞬间巨量分配)。
内存泄漏 → 堆持续涨不回落;内存溢出 → 一次分配爆缸。
泄漏是慢病,溢出是绝症。泄漏不一定会立刻溢出,但溢出一定包含泄漏(或瞬间巨量分配)。