1、内存泄漏基础:什么是内存泄漏、GC原理与引用链、内存泄漏 vs 内存溢出
大家好,我是你们的老朋友。今天咱们正式开讲内存泄漏。
说实话,我做了这么多年Android开发,最头疼的问题之一就是内存泄漏。它不像崩溃那样直接,但就像温水煮青蛙,慢慢把你的App拖垮。嗯,咱们先从最基础的概念说起。
1.1 什么是内存泄漏?
先问大家一个问题:你租了一间房子,退租的时候,是不是得把钥匙还给房东?
内存泄漏说白了就是:你借了内存不还。
在Java/Android世界里,对象用完之后,应该被垃圾回收器(GC)回收。但如果某个对象明明不再需要了,却还被其他对象引用着,GC就没法回收它。这块内存就“漏”掉了。
内存泄漏的定义:程序中已不再使用的对象,因为仍然被引用,导致GC无法回收,造成内存浪费。
我在项目中遇到过这样一个案例:一个Activity退出了,但里面有个匿名内部类持有了它的引用。结果Activity没法被回收,每次进入退出都多占几MB内存。用户多操作几次,App就卡得不行了。
1.2 GC原理与引用链
要理解内存泄漏,必须先搞懂GC是怎么工作的。我刚开始学的时候也觉得这东西很玄乎,其实没那么复杂。
1.2.1 GC的根搜索算法
Java的GC用的是根搜索算法(GC Roots Tracing)。它从一组“根”对象出发,沿着引用链往下找。能到达的对象就是“活的”,到不了的就是“死的”,可以被回收。
GC Roots包括:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
你想想看,GC就像是一个清洁工,从门口开始检查每个房间。能走进去的房间就留着,走不进去的就拆掉。
1.2.2 引用链与可达性
从GC Root到某个对象,中间经过的引用路径,就叫引用链。
举个例子:
// 伪代码示意
GC Root → 静态变量 → 对象A → 对象B → 对象C
如果对象C被对象B引用,对象B被对象A引用,对象A被静态变量引用——那对象C就是可达的,不会被回收。
但如果中间某个引用断了:
GC Root → 静态变量 → 对象A (对象B不再被引用)
// 对象B和对象C就变成了不可达,可以被回收
这就是为什么内存泄漏的本质是引用链没有被切断。
个人经验:我排查内存泄漏时,第一件事就是用Android Studio的Memory Profiler抓堆转储(Heap Dump),然后看引用链。哪条链没断,问题就在哪。
1.2.3 四种引用类型
Java里引用的强度分四种,这个很重要:
| 引用类型 | 回收时机 | 使用场景 |
|---|---|---|
| 强引用 | 永不回收(除非不可达) | 普通new出来的对象 |
| 软引用 | 内存不足时回收 | 缓存、图片加载 |
| 弱引用 | 下次GC时立即回收 | Handler、Context引用 |
| 虚引用 | 随时可能回收 | 对象回收跟踪 |
内存泄漏最常见的就是强引用没释放。我建议大家在设计时,能用弱引用就别用强引用,尤其是那些生命周期长的对象。
1.3 内存泄漏 vs 内存溢出
这两个概念经常被搞混。我刚开始学的时候也分不清,后来踩了坑才明白。
1.3.1 内存溢出(OOM)
内存溢出就是内存真的不够用了。App申请内存时,系统说“抱歉,没了”,然后就抛OOM异常。
OOM的直接原因:
- 一次性加载了大图片或大文件
- 创建了太多线程
- 内存泄漏累积导致可用内存耗尽
1.3.2 两者的关系
说白了:内存泄漏是原因,内存溢出是结果。
你想想看,每次泄漏一点点,一次两次没事。但泄漏100次、1000次呢?可用内存越来越少,最后就OOM了。
避坑指南:我曾经接手过一个项目,线上OOM率高达3%。一开始以为是图片加载的问题,查了半天发现是某个单例持有了Activity引用,每次打开页面都泄漏几KB。用户一天打开50次,一周下来就炸了。所以,别小看任何一次泄漏。
1.3.3 对比总结
| 对比项 | 内存泄漏 | 内存溢出 |
|---|---|---|
| 本质 | 对象无法被回收 | 内存耗尽 |
| 表现 | 内存只增不减 | 直接崩溃 |
| 是否可恢复 | 可恢复(释放引用后GC会回收) | 不可恢复(必须重启) |
| 排查难度 | 较难,需要分析引用链 | 较易,看堆栈就知道哪行代码 |
| 常见场景 | 静态变量持有Activity、Handler未移除 | 加载超大Bitmap、无限循环创建对象 |
1.4 小结
这一章咱们聊了三个核心概念:
- 内存泄漏:对象活着但没人用,占着茅坑不拉屎
- GC与引用链:GC从根出发找活对象,引用链不断就回收不了
- 泄漏 vs 溢出:泄漏是病根,溢出是病发
嗯,基础打牢了,后面咱们才能精准定位和修复。下一章我会带大家实战,用工具抓一个真实的内存泄漏案例。到时候你们就知道,这东西其实没那么可怕。
课后思考:你可以在自己的项目里试试,用Memory Profiler抓一次堆转储,看看有没有“该回收却没回收”的对象。发现了别急着修,先分析引用链,找到是谁在“挽留”它。