一、从本质看"内存泄露"是什么
内存泄露在 Java/Android 中的定义 :对象不再被业务需要,但仍然能被 GC 根(GC roots)通过一条或多条引用路径访问到,导致垃圾回收器不会回收它,从而占用内存并可能引起 OOM。在Android中 ,内存泄漏大概就包括Java对象层面的泄漏 和Android系统资源(如Bitmap、Cursor等)的未释放问题。即便 Java 对象被回收,native 资源若没有显式释放也会泄露。
二、底层原理
上面说了,在 Android 中内存泄漏分为「JVM 堆内存泄漏」和「系统资源泄漏」两类, JVM 管理的对象由 GC 回收,而 Android 系统资源则由开发者手动调用 API 释放,系统不自动回收。
具体来说 Java/Kotlin 定义的对象 (如 Activity、View、String、集合等),都存储在 JVM 堆中,完全由 GC 管理。比如 Activity 实例、MaterialButton 对象、EventBus 订阅者引用,这些都是 JVM 堆对象,GC 会根据 "可达性" 判断是否回收;
Android 系统资源 是通过 Native 层调用 Linux 内核接口申请的「非 JVM 资源」,GC 既感知不到,也无法回收,常见有系统服务引用 ContentResolver.Cursor、MediaPlayer、广播接收器(BroadcastReceiver)、NotificationManager ;图形资源 :Bitmap(Native 层像素数据)、Drawable、Canvas ;SharedPreferences 的 Editor的内存块等 ,这些都是需要我们自行调api手动关闭的。
2.1.引用类型决定回收
Java中是通过可达性分析判定对象是否存活的,学习该算法需要了解Java中的4种引用。Java中引用分为强引用(默认)、软引用、弱引用、虚引用。我们用到的引用基本都是强引用,其他三种几乎不会出现在日常开发中。
- 强引用:强引用是在程序代码中普遍存在的,例如
Person person = new Person(),只要强引用还存在,垃圾收集器永远不会回收被引用的对象。 - 软引用:软引用是用来描述一些还有用但并非必须的对象,对于软引用关联的对象,在系统将发生内存溢出异常前,将会把这些对象列入回收范围之中进行二次回收,如果该次回收还没有足够的内存,才会抛出内存溢出异常 。Java提供了**
SoftReference**实现软引用,我们在安卓开发中可能用过Glide库处理图片,Glide的底层就用到了软引用。 - 弱引用:弱引用也是描述非必需对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集之前 ,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java提供了**
WeakReference**实现弱引用,Android消息机制中的ThreadLocal就用到了弱引用,ThreadLocal的核心在于其内部类ThreadLocalMap。这个 Map 使用一个特殊的Entry类来存储键值对,而Entry就继承自WeakReference。 - 虚引用:虚引用是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对生存时间构成影响。为一个对象设置虚引用关联的唯一目的就是在这个对象被收集器回收时收到一个系统通知 。Java提供了
PhantomReference实现虚引用。
2.2.GC roots
GC Roots(GC 根节点)是 JVM 垃圾回收器(GC)判断对象是否「可达」的核心起点 、绝对存活起点------GC 会从这些根节点出发,遍历所有可达的对象引用链。
在Java中,可作为GC Roots的对象包括系统栈、静态变量、Classloader、JNI 全局引用、正在运行的线程等。被这些根可达的对象被视为"活着"。
当一个对象到GC Roots没有任何引用链相连时,该对象就是不可用的,此时就会被判定为可回收的对象。这就是Java采用的可达性分析算法。
2.3.生命周期错配
生命周期错配指的是一个生命周期长的对象 (如单例、静态变量、后台线程)不必要地持有了一个生命周期短的对象(如Activity、Fragment)的引用,导致后者在其本该被销毁时无法被垃圾回收器(GC)回收,从而持续占用内存。
三、常见泄露模式
下面分析在实际开发中最常见的几种内存泄漏情况
-
Activity/Context 泄露(最常见)
在 Android 开发中,Activity 或 Context 被无意中持有而导致无法被垃圾回收器(GC)回收,是最常见的内存泄漏问题之一。这通常是因为一个生命周期比 Activity 更长的对象,持有了该 Activity 的引用,从而阻碍了其正常销毁和回收。
单例模式导致泄漏是该情况下经典的泄漏场景,单例对象通常随应用进程一直存在,如果它错误地持有了一个 Activity 的 Context,那么这个 Activity 即使被关闭也无法被回收。这就是因为Activity 被 GC Roots 形成的引用链 "绑定",无法被标记为可回收。看下面的代码
javapublic class AppSettings { private static AppSettings sInstance = new AppSettings(); private Context mContext; public void setup(Context context) { this.mContext = context; // 如果传入的是 Activity.this,则发生泄漏 } }因此在单例情况下,尽量使用系统级别的
contexttypescriptpublic void setup(Context context) { this.mContext = context.getApplicationContext(); // 正确:使用应用级别的 Context } -
匿名内部类 / 非静态内部类
非静态内部类会隐式持有外部类引用(例如
new Runnable(){}持有外部 Activity),如果Runnable被延迟执行或放到持久容器里就会泄露外部Activity。kotliln语言中在一个类内部再声明一个类,这个内部的类就被设计地区别于Java的内部类,称为嵌套类,不持有外部类引用,类似Java中的静态内部类,这种设计原因很大方面就是为了避免内存泄漏。举例下面的代码。如果在Activity销毁前没有处理完队列中的消息,那么就会造成内存泄漏。因为
Handler初始化时会默认关联当前线程的Looper和MessageQueue(主线程的 Looper 随应用进程生命周期存在,永不销毁),只要 Handler 的消息队列中还有未处理的消息,Message会持有Handler的引用,而 Handler 又持有 Activity 的引用,导致 Activity 无法回收 。scalapublic class MainActivity extends Activity { private Handler mHandler = new Handler() { // 匿名内部类,隐式持有 Activity 引用 @Override public void handleMessage(Message msg) { // ... 处理消息 } }; }针对这种情况我们需要在onDestroy中移除所有的消息和回调。
typescript@Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); // 移除所有消息和回调 } -
未注销的系统回调
这种情况和上面的匿名内部类类似,例如
BroadcastReceiver、ContentObserver、LocationListener、ViewTreeObserver等没有在onDestroy/onStop中unregister。这是因为系统服务(如SensorManager、LocationManager)或全局组件(如广播管理器)通常具有与应用程序进程相同的长生命周期。当您在 Activity 中注册一个监听器时,这个长生命周期的服务会持有该监听器的引用。如果这个监听器是 Activity 的非静态内部类或匿名内部类,它会隐式持有其外部类(即 Activity)的强引用。
四、MVP架构内存泄漏剖析
有了前面的理论基础,我们来分析一下MVP架构的内存泄漏问题。
MVP架构中Activity / Fragment 持有 Presenter,而Presenter 持有 View 接口(通常是 Activity),也就是Activity → Presenter → Activity形成了强引用环。只要其中一个地方没有正确断开, Activity 就算退出了,GC 也回收不了 ,会直接导致内存泄漏。因此MVP架构天生容易内存泄漏。我们下面具体举个例子。
这是P层,通过持有的view引用展示数据。
kotlin
class MainPresenter(private val view: MainContract.View) {
fun loadData() {
view.showLoading()
}
}
而View层(MainActivity)通过presenter = MainPresenter(this)也会持有P层的引用。
kotlin
class MainActivity : AppCompatActivity(), MainContract.View {
private val presenter = MainPresenter(this)
override fun onDestroy() {
super.onDestroy()
// 没有 presenter.detachView()
}
}
在这种情况下当Activity还存在时, V 层被 P 层强引用,P 层又被 V 层强引用 ,形成了强引用环。我们上面讲了Java的垃圾回收用的是可达性分析算法,这种算法对比引用计数算法来说是不怕成环的,但是问题在于,如果P层与某个存活对象建立了关系(比如线程,监听器,消息队列等),这时便不会被清除,P层和V层便会达到永生,造成了内存泄漏。
因此MVP架构一定要注意架构的写法规范
Presenter层:
kotlin
abstract class BasePresenter<V> {
protected var view: V? = null
fun attachView(v: V) {
view = v
}
fun detachView() {
view = null
onCleared()
}
open fun onCleared() {}
}
Activity层:
kotlin
override fun onCreate(...) {
presenter.attachView(this)
}
override fun onDestroy() {
presenter.detachView()
super.onDestroy()
}
五、内存泄露的发现
尽管熟悉了内存泄露,但在开发过程中我们仍不可避免地会偶尔大意造成内存泄漏,这方面有一些工具可以帮助我们监测app的内存问题。
1.Android Studio内置的Memory Profiler
这是最核心的工具。它可以实时显示应用的内存使用量曲线图,帮助你观察内存的总体趋势。你可以通过它捕获堆转储 (Heap Dump) ,获取某个时间点内存中所有对象的快照,从而分析哪些对象可能被不当持有 。此外,它还能记录内存分配情况,追踪对象是在哪部分代码中被创建的,这对于定位泄漏源头非常有帮助 ,如图,对Memory Usage进行分析,如下两图。


不过注意,Profiler 的 Leaks 计数只检测 Activity/Fragment 泄漏,而 App 还可能存在其他泄漏场景,普通对象泄漏 :如单例持有工具类对象、未清理的集合缓存;系统资源泄漏 :如未关闭的 Cursor、未回收的 Bitmap、未注销的监听器;短期泄漏:如 Handler 延迟消息等
2. 自动化检测库:LeakCanary
上面介绍的Profiler毕竟是「手动分析工具」,而 LeakCanary 是自动检测内存泄漏的库 ,它可以自动化地检测内存泄漏,大大提升了开发效率 。只需在项目的 build.gradle文件中添加依赖,LeakCanary 就会自动在应用运行时监控 Activity 和 Fragment 等对象。当这些对象本应被回收却依然存活时,LeakCanary 会触发堆转储,分析泄漏轨迹,并通过通知栏清晰地展示出来,直接定位到泄漏的引用链。
arduino
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
}
编译并运行应用后,LeakCanary 会自动在后台启动。当检测到内存泄漏时,会在通知栏显示通知,点击通知即可查看详细的泄漏分析报告。