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抓一次堆转储,看看有没有“该回收却没回收”的对象。发现了别急着修,先分析引用链,找到是谁在“挽留”它。