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没崩就不用管泄漏」,这是典型的侥幸心理。

嗯,这一章的基础概念就讲到这里。下一章我会带你实战,用工具把泄漏揪出来。