2. 内存泄漏基本概念与Android内存模型归纳总结

做 Android 开发几年,你要是没被内存泄漏坑过,那运气真不错。我身边不少同事,包括我自己,都曾在内存泄漏上栽过跟头——尤其是做相机相关功能的时候,一拍照内存就涨,拍着拍着 App 就挂了。

内存泄漏到底是怎么回事?Android 的内存模型又长什么样?这章咱们把这两个基础问题彻底聊透。

2.1 什么是内存泄漏

内存泄漏,说白了就是——对象已经「死」了,但 GC 却收不了它。

正常情况下,一个对象不再被任何引用指向,GC 就会把它回收。但如果某个本应被释放的对象,还被一个长期存活的对象(比如 static 变量)强引用着,GC 就认为它「还在用」,于是它就赖在堆里不走。

我刚开始做 Android 时,对这事理解得很浅。觉得不就是对象没释放嘛,至于那么大惊小怪?直到有一次我负责的 Camera 应用在连续拍照后直接闪退,dump 下来的 hprof 文件里发现了几百个 Surface 对象排着队等着被回收……嗯,从那以后我再也不敢小看内存泄漏了。

内存泄漏的核心定义: 程序中已不再使用的对象,因为被 GC Roots 强引用持有,导致垃圾回收器无法将其回收。

2.2 内存泄漏 vs 内存溢出

这两个概念经常被混为一谈,其实它们的关系更像是「因」与「果」。

概念 定义 关系
内存泄漏(Leak) 对象无法被回收,占用内存持续增长 是「因」
内存溢出(OOM) 可用内存不足,无法分配新对象 是「果」

你想想看,内存不会凭空消失。每次泄漏一点点,日积月累,堆里就塞满了「僵尸对象」。等到系统想分配一块新内存时,发现凑不够了——OOM 就来了。

我的经验: 遇到 OOM 时,别只盯着「内存不够」这个表象。百分之九十的 OOM 背后,都藏着至少一处内存泄漏。先查泄漏,再谈优化,这个顺序别搞反。

2.3 Android 内存模型概览

Android 的内存模型,我习惯把它拆成两大部分来理解:Java 堆和原生堆。这两块的管理方式完全不同,搞混了就容易出问题。

  • Java 堆(Java Heap)
    由 ART / Dalvik 虚拟机管理。对象实例、数组都在这里分配。GC 负责自动回收。
  • 原生堆(Native Heap)
    通过 malloc / new 直接分配。Bitmap 的像素数据、OpenGL 纹理、Surface 的 Buffer 都存放在这里。不受 GC 管理,必须手动释放。

这里有个关键点:GC 只管 Java 堆,管不了 Native 堆。很多人在 CameraX 开发中遇到的泄漏,其实都出在 Native 堆上。

避坑指南: 我曾经在 CameraX 项目中遇到过这种情况——SurfaceView 的 Buffer 反复申请却不释放,Native Heap 一路飙升到 1.2 GB,最后直接被系统 LMK 杀进程。用 MAT 分析 Java 堆却干干净净,根本看不出问题。这就是典型的 Native 泄漏,GC 管不了,得靠其它工具才能抓到。
Android 内存模型与内存泄漏关系 Android 内存模型 Java 堆 GC 机制 原生堆 内存泄漏 常见原因 静态引用 · 内部类 · 资源未关闭 · Handler ⚠ 危害:OOM · 卡顿 · 进程被 LMK 杀

2.4 常见内存泄漏模式

在实际项目中,我总结了下面几种最常见的「泄漏套路」。你看看自己踩过几个?

  1. 静态引用持有 —— static 变量引用了 Activity 或 Context,导致整个界面无法释放。
  2. 内部类隐式引用 —— 匿名内部类或非静态内部类,悄悄持有外部类的引用。
  3. 资源未关闭 —— Cursor、File、InputStream、MediaCodec 等用完后没 close。
  4. Handler / 定时任务 —— 延迟的 Message 或 Runnable 持有外部引用,Activity 退出了还赖着不走。
  5. 注册未解绑 —— BroadcastReceiver、ServiceConnection、ContentObserver 注册了没取消。

用一段简单的代码来展示「静态引用 + 内部类」的典型场景:

public class CameraActivity extends AppCompatActivity {
    private static Handler sHandler = new Handler();
    private static Runnable sRunnable = new Runnable() {
        @Override
        public void run() {
            // 这个 Runnable 隐式持有外部类引用
            // 而外部类又被 static 的 Handler 持有
            // 导致 Activity 退出后无法被 GC 回收
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        sHandler.postDelayed(sRunnable, 30000);
    }
}
避坑指南: 我曾经在 CameraX 的连续拍照回调里,用匿名内部类去更新 UI,结果拍照越拍越卡,最后直接 OOM。查了半天才发现是匿名内部类持有了 Activity 引用,而 Activity 因为相机预览的 Surface 还被引用着,根本释放不掉。

2.5 内存泄漏的危害

危害其实就三点,但每一点都够你喝一壶的:

  • OOM 崩溃 —— 最直接的后果,用户直接看到「抱歉,应用已停止运行」。
  • 应用卡顿 —— GC 被频繁触发,每次 GC 都会暂停应用线程(STW),界面掉帧、响应变慢。
  • 进程被 LMK 杀死 —— 系统内存紧张时,Low Memory Killer 会优先干掉占用内存最大的进程,就是你。

做个简单计算:每次泄漏 2 MB,用户正常使用 10 分钟泄漏 20 次,那就是 40 MB。中低端机型的总堆大小可能也就 128 MB,再来几个大 Bitmap,OOM 几乎是必然的。

一句话总结: 内存泄漏不是「可能出问题」,而是「迟早出问题」。早排查,早解决,别等线上爆了再拍大腿。