第1章 内存泄漏基础知识
1.1 现象识别:内存泄漏的典型表现
内存泄漏在应用运行中主要表现为:
- 持续上升的内存占用 —— GC 无法回收已经不再需要的对象,堆使用率随操作次数线性增长。
- 频繁 Full GC / 卡顿 —— 堆中存活对象过多,触发更多次 STW(Stop-The-World)压缩。
- 突然的 OOM(OutOfMemoryError) —— 当已用堆接近上限且无可释放对象时,抛出溢出错误。
- 系统资源耗尽 —— Android 背景下,Activity 泄漏会导致窗口、Receiver、Service 等资源无法释放。
判断规则:如果某类对象实例数在多次 GC 后仍然增长,且引用链中存在非预期长生命周期对象,即可认定泄漏。
1.2 数据采集:内存模型与 GC 根
Java / Kotlin 内存模型分为三个主要运行时数据区:
| 区域 | 存储内容 | GC 参与 |
|---|---|---|
| 堆(Heap) | 所有 new 的对象、数组、Kotlin 的 object 实例 | 是(主要回收区域) |
| 栈(Stack) | 局部变量、方法调用帧、基本类型 | 否(栈帧弹出即释放) |
| 方法区(Metaspace) | 类元数据、常量池、静态变量、Kotlin companion object | 是(类卸载时) |
GC 根(GC Roots) 是可达性分析的起点,包括:
- 栈中引用的对象(局部变量、参数)
- 静态变量(类加载器)
- 活跃线程(Thread)
- JNI 全局引用
- 系统类(Bootstrap ClassLoader)
Kotlin 注意:
object 声明、companion object 以及顶层扩展属性在编译后均会生成静态字段(在方法区中),极易成为意外 GC 根,需格外关注。
1.3 原因定位:可达性分析
GC 从所有 GC 根出发,沿着引用链搜索。能到达的对象标记为存活,不可达 的对象即为可回收垃圾。泄漏本质是:本应不可达的对象仍被一个或多个 GC 根直接或间接引用。
1.4 修复动作:切断引用链
找到泄漏链后,通过以下方式修复:
- 静态引用 → 改用
WeakReference或SoftReference。 - 内部类 / Lambda → 使用静态内部类 + 弱引用,或显式在 onPause/onDestroy 中置空。
- 注册/注销 → 确保在生命周期结束前取消注册(如 BroadcastReceiver、SensorListener)。
- Kotlin object / companion object → 避免持有 Activity 强引用,改用 Application Context。
// 错误:静态持有 Activity
private static Activity activity;
// 修复:使用弱引用
class MyApplication {
private val activityRef = WeakReference<Activity>(null)
private fun setActivity(act: Activity) { activityRef = WeakReference(act) }
private fun getActivity(): Activity? = activityRef.get()
}
1.5 复测:验证泄漏解除
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1. 抓取前后堆转储 | 修复前运行场景 → dump hprof;修复后重复场景 → dump hprof | 差异文件 |
| 2. 比较实例数 | 在 MAT 中对比两次堆转储的 Activity 实例数 | 修复后实例数稳定 |
| 3. 检查 GC 根路径 | 对可疑对象执行“Merge Shortest Paths to GC Roots” | 修复后无意外长路径 |
| 4. 压力测试 | 反复进入/退出页面 20 次,观察内存曲线 | 不再持续增长 |
数据采集提示:使用
adb shell dumpsys meminfo 或 Android Profiler 的 Memory Profiler 快速定位暴涨类。
1.6 预防:内存泄漏编码规范
从设计层面杜绝常见泄漏源:
- 慎用静态变量 —— 除非是不可变单例且不持有 UI 类引用。
- 优先 WeakReference —— 缓存、回调、监听器中替代强引用。
- 生命周期同步 —— 非静态内部类、匿名类、Lambda 若需要引用外部对象,确保在该对象销毁前切断。
- Kotlin 特殊关注点
object类 → 本质是静态单例,类中属性全部静态。companion object→ 编译为静态字段。by lazy委托 → 如果延迟初始化捕获了外部 this,则隐式持有。
- 使用工具 —— LeakCanary 自动检测,MAT 深度分析。
常见误区:认为 Kotlin 没有静态关键字就不会泄漏——编译器生成的静态字段(companion、object、顶层属性)同样会成为 GC 根,务必警惕。