1、内存泄漏基础:什么是内存泄漏、为什么Android容易发生内存泄漏、内存泄漏与内存溢出的区别
1.1 什么是内存泄漏?
内存泄漏,说白了就是「本该被回收的对象,却一直活在内存里」。
我习惯用一个比喻来解释:你租了一间仓库,用完东西后应该搬走。但有些东西你忘了搬,仓库管理员(GC)也不敢扔,因为你还拿着钥匙(引用)。久而久之,仓库越来越满,新东西放不进去了。
在Android里,内存泄漏就是某个对象不再需要了,但因为有强引用链连着它,GC(垃圾回收器)没法回收。这个对象就变成了「僵尸」,占着内存不干活。
核心定义:内存泄漏 = 无用对象 + 强引用 + GC无法回收
举个例子,你写了一个Activity,里面开了一个线程做耗时操作。Activity关闭后,线程还在跑,并且持有Activity的引用。这时候Activity就泄漏了。
// 典型的内存泄漏示例
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 匿名内部类持有外部Activity的引用
new Thread(new Runnable() {
@Override
public void run() {
// 模拟耗时操作
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
这段代码里,Runnable是匿名内部类,它隐式持有MainActivity的引用。线程一直在跑,Activity就永远无法被回收。嗯,这就是最经典的内存泄漏场景。
1.2 为什么Android容易发生内存泄漏?
这个问题我思考过很多次。Android内存泄漏这么普遍,不是开发者不努力,而是平台本身就有几个「坑」。
第一,Activity和Fragment的生命周期复杂。
你想想看,一个Activity从创建到销毁,中间可能经历配置变更、后台回收、用户主动关闭等各种情况。任何一个环节没处理好,引用链没断开,泄漏就发生了。我在项目中遇到过,一个简单的屏幕旋转,就能让Activity泄漏好几次。
第二,Context的滥用。
Context是Android的「万能钥匙」,但也是泄漏的「重灾区」。很多开发者习惯把Activity的Context传给单例、静态变量或者长生命周期的对象。Activity销毁了,Context还被别人拽着,能不泄漏吗?
避坑指南:我曾经接手过一个项目,整个代码库里到处都是 static Context。结果就是,随便点几下页面,内存就涨了上百兆。后来我花了整整两天,才把所有泄漏点清理干净。
第三,匿名内部类和Handler。
Java的匿名内部类会隐式持有外部类的引用。这在Android里特别危险,因为很多异步操作(线程、Handler、AsyncTask)都用了匿名内部类。外部类(比如Activity)都销毁了,内部类还在后台跑着,泄漏就来了。
第四,资源对象未及时关闭。
BroadcastReceiver、ContentObserver、Cursor、各种Listener……这些资源用完后必须解注册或关闭。很多人只记得注册,不记得解注册。结果就是,对象虽然没用了,但系统还留着它的引用。
| 常见泄漏场景 | 根本原因 | 我遇到的频率 |
|---|---|---|
| Handler + 匿名内部类 | 内部类持有Activity引用 | 极高 |
| 静态变量持有Activity | 生命周期不匹配 | 高 |
| 单例持有Context | 长生命周期对象持有短生命周期引用 | 高 |
| 未解注册的Listener | 资源未释放 | 中等 |
| 线程/异步任务 | 任务未结束,引用未断开 | 高 |
1.3 内存泄漏与内存溢出的区别
这两个概念经常被搞混。我面试别人的时候,十个人里有六七个分不清。其实很简单:
- 内存泄漏(Memory Leak):该回收的对象没回收,内存被「占着茅坑不拉屎」。
- 内存溢出(Out Of Memory,OOM):内存真的不够用了,新对象申请不到空间,直接崩溃。
它们的关系是:内存泄漏是原因,内存溢出是结果。
你想想看,内存泄漏一点一点地积累,就像水龙头没关紧,水滴个不停。一开始没事,但时间长了,水池(堆内存)就满了。满了之后,新来的水(新对象)就没地方放了,只能溢出来——这就是OOM。
一句话总结:内存泄漏是「该回收的没回收」,内存溢出是「想分配但没空间了」。泄漏多了,必然导致溢出。
举个例子:你的App正常只需要50MB内存。但因为有泄漏,每次打开一个页面就多占5MB。打开10次后,多占了50MB。这时候系统给App分配的最大堆内存是128MB,加上其他开销,可能就接近极限了。再操作一下,OOM就来了。
我记得有一次线上反馈,说某个页面打开超过20次就必崩。我一看堆栈,是OOM。用MAT分析dump文件,发现同一个Activity的实例有20多个。说白了,就是每次打开都泄漏一个,20次后堆内存撑爆了。
我的经验:排查OOM问题时,不要只盯着「哪里分配了大内存」。很多时候,OOM的罪魁祸首是内存泄漏。先把泄漏修了,OOM自然就消失了。我处理过的OOM案例里,大概有70%都是泄漏引起的。
最后说一个容易忽略的点:内存泄漏不一定会立刻导致OOM。 有些泄漏很小,每次只漏几KB,App跑一整天都没事。但用户如果长时间使用,或者手机内存本身就不大,泄漏的累积效应就会显现出来。所以,不要觉得「App没崩就不用管泄漏」,这是典型的侥幸心理。
嗯,这一章的基础概念就讲到这里。下一章我会带你实战,用工具把泄漏揪出来。