安卓开发中常见内存泄漏场景分析及LeakCanary工具原理解析

前些天,有人询问“开发过程中常见的内存泄漏都有哪些?”。随后,我脱口而出:当静态的对象(包含单例)持有一个生命周期较短的引用时,会导致内存泄漏;内部类的子代码块对象的生命周期超过外面代码的生命周期(像非静态内部类、线程等情况),也会导致这个短生命周期的对象出现内存泄漏。一个对象的生命周期结束,也就是不再使用该对象了,之后却依然有某些对象持有该对象的强引用,这种场景就是内存泄漏。

安卓开发中常见内存泄漏场景分析及LeakCanary工具原理解析

这样的回答显然不是问答人所期望的包含哪些场景。因此,我抽空整理了内存相关的知识点,以及 LeakCanary 工具的原理分析。

Java内存问题 及 LeakCanary 原理分析

在安卓等其他移动平台上,内存问题具有特别重要的地位。要实现虚拟机内存的高效利用,并且能够快速定位内存问题,了解虚拟机内存模块及管理的相关知识是很有必要的。这篇文章会从最基础的知识开始分析,包括内存问题产生的地方、原因以及解决方案等原理。

一、运行时内存区域

以 Java 虚拟机作为例子,将运行时的内存划分成不同的区域,并且每个区域都担负着不同的功能。

方法区用于存储虚拟机已加载的类信息、常量、静态常量以及即时编译器编译后的代码等数据。异常状态为 OutOfMemoryError,此状态包含常量池以及用户存放编译器生成的各种字面量和符号引用。

JVM 所管理的内存中最大的一块是堆。堆的唯一目的是存放实例对象,几乎所有的对象实例都在这里被分配。Java 堆是垃圾收集器管理的主要区域,所以很多时候它也被称作“GC 堆”。存在异常状态 OutOfMemoryError。

每一个方法从开始调用到最终完成的整个过程,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。定义了两种异常状态 OutOfMemoryError 和 StackOverflowError 针对这个区域。

本地方法栈和虚拟机栈的作用不同,本地方法栈为虚拟机使用到的 Native 方法服务,虚拟机栈则为虚拟机执行 Java 方法。异常状态有 StackOverFlowError 和 OutOfMemoryError。

程序计数器是一块较小的内存。它是当前线程所执行的字节码的行号指示器。字节码解释器工作时,会通过改变这个计数器的值,来选取下一条需要执行的字节码指令。

内存模型

Java 内存模型规定所有变量存储于主内存。每条线程有自己的工作内存。线程的工作内存保存该线程使用的变量,这些变量从主内存拷贝而来。线程对变量的操作(读、写)需在工作内存中进行。不同线程无法直接访问对方工作内存中的变量。线程间变量值的传递需通过主内存完成。

为保证内存可见性,常借助_volatile_关键子的特性来确保变量的可见性,不过它不能保证并发时的原子性。

二、内存如何回收 内存的分配

最后进入对象空间重分配阶段为 De-allocated。

内存的分配发生在创建阶段。在这个阶段,首先要用类加载器加载目标 class。当通过加载器检测后,就会开始为新对象分配内存。对象分配内存的大小在类加载完成后就能够确定。当初始化完成后,虚拟机还需要对对象进行必要的设置,比如确定是哪个类的实例,如何查找元数据,以及对象的 GC 年代等。

内存的回收(GC)

那些对象不可能再被任何途径使用了,它们需要被回收。不然的话,内存迟早会被消耗空。

GC 机制主要是运用可达性分析法。它以一系列被称作“GC Roots”的对象作为起始点,从这些节点开始往下进行搜索。搜索过程中所经过的路径被称为引用链。当一个对象与 GC Roots 之间不存在任何引用链时,也就是说从 GC Roots 到该对象无法到达,这就证明了此对象是不可达的。

《深入理解 Java 虚拟机》这本书中描述,可作为 GC Root 的地方有以下这些:

一个对象或几个相互引用的对象组若没有任何引用链,就会被当作垃圾处理,并且可以进行回收。

如果一个对象在程序中已不再使用,然而(强)引用仍被其他对象持有,那么就称作内存泄漏。内存泄漏不会让程序立即出现异常,不过多处未处理的内存泄漏可能会引发内存溢出,进而导致不可预估的后果。

引用的分类

JDK1.2 之后,为了优化内存的利用以及提高 GC 的效率,Java 对引用的概念进行了扩充。Java 把引用分为强引用、软引用、弱引用、虚引用这 4 种。

强引用存在时,垃圾收集器永远不会回收被引用的对象。

软引用的情况是,在系统即将发生内存溢出异常之时,会将这些对象列入回收范围以进行二次回收。倘若此次回收仍未获得足够内存,才会抛出内存溢出异常。SoftReference 指的就是软引用。

弱引用的特点是,只要存在垃圾回收(GC),不管当前内存是否足够,都会回收掉仅仅被弱引用关联的对象。其中,WeakReference 这个概念表示的就是弱引用。

虚引用存在的唯一目的是,在该对象被收集器回收时能收到一个系统通知。被虚引用关联的对象,其生存时间与虚引用毫无关系。PhantomReference 指的是虚引用,它需要与 ReferenceQueue 配合使用,以检测对象的回收情况。

关于JVM内存管理的一些建议

手动将无用对象尽可能地置为 null,以此来加快内存回收。可以考虑使用对象池技术生成可重复使用的对象,这样能减少对象的生成。要合理地利用四种引用。

三、内存泄漏

持有生命周期较短的引用时,或者内部子模块对象的生命周期超过外面模块的生命周期,本该被回收的对象就不能被回收而停留在堆内存中,这会产生内存泄漏。

内存泄漏是导致应用程序 OOM 的主要原因之一。在像安卓这样的移动平台,应用难免会出现所需要的内存超过系统分配的内存限额的情况,这种情况会造成内存溢出 Error。

安卓平台常见的内存泄漏

静态成员变量会持有外部(短周期临时)的对象引用。例如,单例类(类内部的静态属性)持有一个 activity(或者其他短周期对象)的引用,这样就会使得被持有的对象内存无法得到释放。

内部类方面。若内部类与外部类的生命周期不相同,就会引发内存泄漏。例如非静态内部类创建了静态实例,以及在 Activity 中的 Handler 或者 Thread 等情况。

资源未及时进行关闭操作。例如数据库方面,以及 IO 流、Bitmap 等,还有注册的相关服务、webview 以及动画等。

4、集合内部Item没有置空。

5、方法块内不使用的对象,没有及时置空。

四、如何检测内存泄漏

Android Studio 提供了诸多用于 App 性能分析的工具,能够便捷地对 App 性能进行分析。我们能够运用 Memory Monitor 和 Heap Dump 去观察内存的使用状况,利用 Allocation Tracker 来追踪内存分配的情况,同时还可以借助这些工具找到疑似出现内存泄漏的位置。

堆存储文件(hpof)能够通过 DDMS 或者 Memory Monitor 来生成,其输出的文件格式为 hpof。MAT(Memory Analysis Tool)是用于分析堆存储文件的。

然而分析内存问题借助 MAT 工具并非易事,需具备一定经验来进行引用链的分析,且有一定门槛。随着安卓技术生态不断发展,LeakCanary 开源项目应运而生,只需将几行代码引入目标项目,便能自动分析 hpof 文件,进而将内存泄漏的位置展示出来。

五、LeakCanary原理解析

LeakCanary 内存检测工具是一个著名的项目,它是由 squar 公司开源的。这里主要对其源码实现原理进行分析。

基本原理

主要是在 Activity 的&onDestroy 方法里,会手动去调用 GC。接着利用 ReferenceQueue 与 WeakReference 这两者,以此来判断是否存在释放不掉的引用。之后再结合 dump memory 的 hpof 文件,通过 HaHa 来分析出泄漏的地方。

源码分析

LeakCanary 的集成较为便捷,仅需几行代码,故而能够从入口开始对代码进行跟踪,并且可以对其原理进行分析。

如果(WeiboApplication.this 不处于 LeakCanary 的分析器进程中) {

LeakCanary 安装在 WeiboApplication.this 上。

Application application 为安装的应用程序,公共静态的 RefWatcher 可通过该应用程序进行安装。

return (refWatcher(application) 为 AndroidRefWatcherBuilder 类型)

配置监听器及分析数据格式,使用 listenerServiceClass 为 DisplayLeakService.class,并且使用 excludedRefs 为 AndroidExcludedRefs.createAppDefaults().build()

.buildAndInstall();

从这里能看出,LeakCanary 会开启一个单独的进程,这个进程用于执行分析任务,并且监听任务是与分析任务分开来进行处理的。

方法_install_主要是构建出一个 RefWatcher 。

最后将构建好的 RefWatcher 安装到合适的位置。

RefWatcher 是通过 this.build()构建出来的。

如果 refWatcher 不等于 RefWatcher.DISABLED ,那么

在当前的上下文环境下,开启显示内存泄漏活动的功能,即调用 LeakCanary 的 enableDisplayLeakActivity 方法,并传入 this.context 作为参数。

ActivityRefWatcher 安装了((Application)this.context, refWatcher)。

return refWatcher;

静态的 void 方法 install 接收一个 Application 类型的参数 application 和一个 RefWatcher 类型的参数 refWatcher 。

创建一个新的 ActivityRefWatcher 对象,该对象接收 application 和 refWatcher 作为参数,然后调用这个对象的 watchActivities 方法。

在活动创建时会调用该方法,该方法接收一个 Activity 对象和一个 Bundle 对象作为参数,分别表示活动本身以及活动的保存状态实例。

公共的 void 类型的方法 onActivityStarted,它接收一个 Activity 类型的参数 activity 并执行相应操作。

公共的 void 方法 onActivityResumed,其参数为 Activity 类型的 activity,此方法为空实现。

在 Java 编程中,存在一个名为 public void onActivityPaused(Activity activity) 的方法,该方法用于处理活动暂停的相关逻辑,在活动暂停时会被调用,它接收一个 Activity 类型的参数 activity ,通过这个参数可以对当前暂停的活动进行相关操作和处理。

当活动停止时,会调用这个方法,参数是一个 Activity 对象。这个方法的作用是在活动停止时进行一些相关的处理操作。它没有具体的实现内容,只是一个空的方法定义,具体的功能需要在子类中进行重写和实现。

这个方法用于在活动状态保存时进行相关操作,它接收一个活动对象和一个用于存储状态的 Bundle 对象作为参数。具体来说,就是在活动的状态需要被保存时,通过这个方法来处理相关的逻辑和数据存储。

在活动被销毁时,会调用这个方法。这个方法接收一个 Activity 类型的参数 activity。它用于在活动被销毁时执行一些相关的操作或处理。

ActivityRefWatcher 自身的 onActivityDestroyed 方法作用于该 activity 上。

};

当活动被销毁时,会调用这个方法。这个方法接收一个 Activity 类型的参数 activity。它主要用于在活动销毁时进行一些相关的清理或处理操作。

this.refWatcher 对 activity 进行了 watch 操作。

Application 的_registerActivityLifecycleCallbacks_方法是具体监听的原理,此方法能够对应用内所有 Activity 的生命周期进行监听,而 LeakCanary 仅仅监听了 Destroy 方法。

在每个 Activity 的 OnDestroy()方法里会回调 refWatcher.watch()方法,我们找到 RefWatcher 的实现类后,来看它具体做些什么。

公共的 void 方法用于观察(被观察的引用),同时指定引用的名称(referenceName)。该方法接收一个被观察的引用(watchedReference)作为参数,用于进行观察操作。

if(this != DISABLED) {

检查 watchedReference 不为 null,因为如果 watchedReference 为 null 会导致错误,所以需要进行该项检查,检查的对象是 watchedReference ,检查的条件是 watchedReference 不为 null ,检查的提示信息是“watchedReference”。

检查 referenceName 不为 null,因为如果 referenceName 为 null 会导致错误,所以需要进行该项检查,确保 referenceName 不为 null 。

System.nanoTime() 被赋值给了 long 类型的 watchStartNanoTime

使用 UUID 生成一个随机的字符串作为 key,以此来保证 key 的唯一性。即 String key = UUID.randomUUID().toString();通过这种方式确保了 key 的独特性。

this.retainedKeys.add(key);

创建了一个 KeyedWeakReference 类型的 reference ,该 reference 是通过 new 关键字初始化的,它的 watchedReference 参数为 watchedReference ,key 参数为 key ,referenceName 参数为 referenceName ,队列参数为 this.queue 。

确保此操作异步完成,传入的参数为 watchStartNanoTime 和 reference 。

final 类 KeyedWeakReference 是 WeakReference 的子类。

public final String key;

public final String name;

KeyedWeakReference 有一个构造函数,它接收四个参数,分别是 referent(被引用对象)、key(键)、name(名称)和 referenceQueue(引用队列)。这个构造函数用于创建一个 KeyedWeakReference 对象,同时 ReferenceQueue 类会监听回收的情况。

this.key 等于将 key 进行非空检查后转换为字符串,即 (String)Preconditions.checkNotNull(key, “key”);Preconditions.checkNotNull 用于确保 key 不为 null,若为 null 则会抛出异常,然后将其转换为字符串赋值给 this.key。

this.name 等于 (String)Preconditions 检查不为空的 name,同时给出“name”的提示。

确保异步消失。需传入一个起始时间(以纳秒为单位) watchStartNanoTime 和一个键控弱引用 reference 。

this 的 watchExecutor 执行一个可重试的任务,即执行一个新的 Retryable 对象。

public Result run() {

RefWatcher.this 确保被引用对象消失;确保消失的操作从 watchStartNanoTime 开始;返回确保消失的结果;确保消失的对象是 reference;返回 RefWatcher.this 确保消失后的结果;确保消失的过程从 watchStartNanoTime 开始;返回 RefWatcher.this 对 reference 确保消失后的结果;确保 reference 消失;从 watchStartNanoTime 开始确保 reference 消失;返回 RefWatcher.this 确保 reference 消失后的结果;确保 reference 从 watchStartNanoTime 开始消失;返回 RefWatcher.this 确保 reference 从 watchStartNanoTime 开始消失后的结果

});

KeyedWeakReference 是 WeakReference 类的子类,它使用了 KeyedWeakReference(referent, key, name, ReferenceQueue )这个构造方法,把要监听的对象(activity)的引用传递进去了,同时还新创建出一个 ReferenceQueue 来监听 GC 之后的回收情况。

以下代码中的 ensureGone()方法乃是 LeakCanary 进行检测回收的核心代码。

确保引用被移除(KeyedWeakReference 引用,从开始监控的纳秒时间 watchStartNanoTime)。

System.nanoTime()被赋值给了 long 类型的变量 gcStartNanoTime 。

首先,计算出 watchStartNanoTime 与 gcStartNanoTime 的时间差,即 gcStartNanoTime - watchStartNanoTime 。然后,将这个时间差从纳秒转换为毫秒,使用 TimeUnit.NANOSECONDS.toMillis 方法。最后,将转换后的毫秒值赋给 long 类型的 watchDurationMs 。

this 从队列中把引用进行 poll 操作;

如果这个调试器控制对象的调试器已附加,那么就进行规避调试模式的操作。

return Result.RETRY;

如果 this.gone(reference) 为真,那么就进行检测是否已经回收。

return Result.DONE;

} else {

//如果没有被回收,则手动GC

手动触发垃圾回收(GC),执行 this.gcTrigger.runGc() 方法。

this 执行 removeWeaklyReachableReferences() 方法;接着再次尝试进行 poll 操作;同时检测是否被回收。

if(!this.gone(reference)) {

// 还没有被回收,则dump堆信息,调起分析进程进行分析

System.nanoTime() 的结果被赋值给了 long 类型的 startDumpHeap 变量。

使用 TimeUnit.NANOSECONDS.toMillis 方法,将 startDumpHeap 与 gcStartNanoTime 的差值转换为毫秒,得到 long 类型的 gcDurationMs 。

this.heapDumper 会生成一个堆转储文件,该文件为 File 类型,这个文件就是 heapDumpFile,即 this.heapDumper.dumpHeap() 的结果。

如果堆转储文件等于堆转储器.稍后重试,那么(执行相应操作);如果堆转储文件不等于堆转储器.稍后重试,那么(执行其他操作)。

return Result.RETRY;//需要重试

最后将这个时间差从纳秒转换为毫秒,并赋值给 long 类型的变量 heapDumpDurationMs,即 TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startDumpHeap)。

this.heapdumpListener 对新的 HeapDump 进行分析,该 HeapDump 包含 heapDumpFile、reference.key、reference.name、this.excludedRefs、watchDurationMs、gcDurationMs 和 heapDumpDurationMs 这些参数;

return Result.DONE;

判断一个 KeyedWeakReference 引用是否已被移除。如果该引用的键对应的弱引用已被垃圾回收器回收,则返回 true;否则返回 false。

判断 this.retainedKeys 是否包含 reference.key,如果包含则返回 false,否则返回 true。

它的主要作用就是进行相关的移除操作。

KeyedWeakReference ref;

当((ref = (键控弱引用)this.queue.poll())不为 null 时) {

ref.key 从 this.retainedKeys 中被移除了。

在方法 ensureGone 里,通过检测 referenceQueue 队列中的引用情况,以判断回收情况,并且通过手动 GC 来进一步确认回收情况。整个这个过程肯定会耗费时间并且会卡住 UI,这个过程会在 WatchExecutor 中执行,而 WatchExecutor 又是在何处执行的呢?

LeakCanary 对 Looper 机制进行了一定的优化,它会在主线程空闲的时候去执行检测任务。现在我们要找到 WatchExecutor 的实现类,然后研究其原理。

公共的最终类 AndroidWatchExecutor 实现了 WatchExecutor 接口。

static final String 是一个常量声明,其中的 LEAK_CANARY_THREAD_NAME 被赋值为“LeakCanary-Heap-Dump”。

创建了一个私有最终的 Handler 对象 mainHandler,这个对象是通过 Looper.getMainLooper()获取到的主 Looper 来初始化的。

一个名为 backgroundHandler 的处理程序是私有的且是最终的。

这个变量是一个私有的最终长整型变量,名为 initialDelayMillis ,它用于表示初始延迟的毫秒数。

私有最终的长整型变量 maxBackoffFactor 。

创建一个 AndroidWatchExecutor 实例,其初始延迟时间为指定的毫秒数(initialDelayMillis)。

HandlerThread 被创建,其名称为“LeakCanary-Heap-Dump”。

handlerThread.start();

这个 backgroundHandler 是一个新创建的 Handler,它使用了 handlerThread 获取到的 Looper 。

This 中的 initialDelayMillis 被设置为 initialDelayMillis 。

this 的 maxBackoffFactor 由 9223372036854775807L 除以 initialDelayMillis 得到 。

公共的 void 方法执行(可重试的 retryable)。

我来做个自我介绍,我于 13 年从上海交大毕业。我曾经有过在小公司工作的经历,也去过华为以及 OPPO 等大型企业。在 18 年的时候进入了阿里,并且一直待到现在。

大多数 Java 工程师想要提升技能,通常是自己摸索着成长,或者选择报班学习。然而,培训机构动辄几千的学费,让他们承受着不小的压力。自己进行不成体系的自学,效果既低效又漫长,并且很容易遇到技术瓶颈,导致技术停滞不前。

收集整理了一份《2024 年 Java 开发全套学习资料》。初衷很简单,是希望能帮助想自学提升却不知从何学起的朋友,还能减轻大家的负担。

有适合小白学习的零基础资料,还有适合 3 年以上经验的小伙伴深入学习提升的进阶课程,这些资料和课程基本涵盖了 95%以上的 Java 开发知识点,是真正体系化的。

文件比较大,所以这里仅将部分目录截图展示。每个节点内都涵盖了大厂面经、学习笔记、源码讲义、实战项目以及讲解视频,这些内容还会持续进行更新。

你若觉得这些内容对你有帮助,便可进行扫码操作以获取!(备注为获取 Java 相关内容)

难道这样就够了吗?不,远远不够!

提前熟悉阿里往年的面试题对面试有很大帮助。作为技术性职业,手里有实打实的技术是面对面试官最有用的利器,这能散发内在的自信。

备战阿里期间,我在学习技术上花费的时间最多,其占我所有学习计划的百分之 70。这些是我在学习期间觉得很不错的学习笔记。

我要写这篇文章的原因是什么呢?我认为学习不能停下脚步。在网络上与大家一起分享和讨论,不仅能够遇到更多志同道合的人,还能拓展自己的眼界,学习到更多的技术。我会在 csdn、博客、掘金等网站上分享技术,这也是一种学习的方式。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

28阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

26阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

26阅读