1 认识内存泄漏:定义、危害、常见场景、与内存溢出的区别

1.1 现象识别 —— 用户与开发者感知的信号

内存泄漏在运行时并不会直接抛出异常,而是通过一系列逐渐恶化的性能现象暴露出来。在排查流程的第一步,需要准确识别这些“早期预警信号”。

现象类别具体表现判断规则
UI 卡顿 / 掉帧操作界面时频繁出现丢帧(>16ms),尤其页面切换后仍然持续使用 Profile GPU Rendering 检测垂直同步红线
GC 频繁 / 停顿长Logcat 出现大量 Background concurrent copying GC freed,且间隔 < 5sadb 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 数据采集 —— 用工具锁定泄漏现场

确认存在内存异常后,执行标准的数据采集流程,为后续定位提供原始证据。

  1. 抓取堆转储(Heap Dump): Android Studio → Profiler → Memory → Dump Java heap。
  2. 获取内存快照命令行:
adb shell am dumpheap com.example.app /sdcard/dump.hprof
adb pull /sdcard/dump.hprof
  1. 记录系统级内存摘要:
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 分析的引用链,可以定位到导致泄漏的“根引用”。下图展示了从分配、残存到崩溃的完整泄漏路径。

内存泄漏 → 内存溢出 故障传递链 对象创建 长生命周期持有 GC 无法回收 堆持续膨胀 OOM 崩溃 核心判断:在 hprof 中查找 “GC Root” 到泄漏对象的引用链。常见 GC Root 包括静态字段、Thread、JNI Global Reference。 若某对象在不该存活的时刻仍存在可达路径,即可判定为内存泄漏。

1.4 修复动作 —— 典型泄漏场景与修复方案

下表列出实际项目中最常见的 5 类内存泄漏场景,并给出对应的修复动作。

场景泄漏原因修复动作
静态变量持有 Activitystatic Context context = this;改为 Application Context;或使用弱引用 WeakReference
匿名内部类(Handler / Runnable)Handler 持有外部 Activity 引用,而 MessageQueue 未清除使用静态内部类 + WeakReference;Activity onDestroy 时调用 handler.removeCallbacksAndMessages(null)
单例模式持有了 View / ContextSingleton 内部缓存了 Activity 级别的对象单例只依赖 Application Context;若必须缓存,注册生命周期回调及时释放
未取消的观察者 / 监听器注册了 EventBus / SensorManager 等,忘记反注册在 Activity/Fragment onDestroy 中执行 unregister()
Bitmap 对象未及时 recycle大量 Bitmap 驻留在 Native Heap 且未释放使用 BitmapFactory.Options.inBitmap 复用;或确保系统 GC 能回收(避免全局引用)
⚠️ 修复陷阱: 不要盲目将 Activity 实例传给 Application。修复前务必使用 hprof 确认引用链,避免引入新的泄漏或空指针。

1.5 复测 —— 验证修复是否生效

  1. 重复执行引发泄漏的操作流程(至少 3 轮进入/退出)。
  2. 使用 Profiler Memory 记录每次操作后的堆大小,观察是否有稳定的回落。
  3. 用 adb shell dumpsys meminfo 对比修复前后的 Pss Total 差值。
  4. 执行 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 导致进程死亡
因果关系累积泄漏最终会引发 OOMOOM 可由一次巨大的对象分配或长期泄漏造成
定位手段Heap Dump 分析引用链分析 hprof 中最大对象,或检查单个分配栈(如大 Bitmap)
典型修复断开不应有的引用减少单次分配、复用对象、降低最大堆限制需求
📐 判断规则口诀:
内存泄漏 → 堆持续涨不回落;内存溢出 → 一次分配爆缸。
泄漏是慢病,溢出是绝症。泄漏不一定会立刻溢出,但溢出一定包含泄漏(或瞬间巨量分配)。