内存泄漏(Memory Leak
)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏会使应用占用的内存,随着时间不断的增加而造成应用 OOM(Out Of Memory)
错误,使应用崩溃。
基础概念
内存分配及垃圾回收 GC
内存分配及垃圾回收的基础知识,参考:JVM 内存分配及垃圾回收-内存分配
小结:
GC
回收的对象必须是不可达的,或者当前没有任何引用的对象- 当对象在使用完成后(对我们而言已经是垃圾对象了),如果没有释放该对象的引用,会导致
GC
不能回收该对象而继续占用内存 - 垃圾对象持续占用内存,导致内存空间的浪费,就发生内存泄露了
- 大量的内存泄露导致有效内存减少,当再次合理申请不到足够内存时,则会出现内存溢出
内存泄露和内存溢出
- 内存泄露
垃圾对象依旧占据堆内存,没有得到正确的释放。水池的水在使用后需要将脏水排空,但是排水管修的太高,有一部分脏水占用了水池的空间。
- 内存溢出
内存占用达到最大值,当再需要时已经无法分配,这就是内存溢出。水池已经满了,这时再放水进来就会溢出。
内存的溢出是内存分配达到了最大值,而内存泄漏是无用内存充斥了内存堆;内存泄漏会占用内存堆导致可用内存太少,很容易出现内存溢出现象。
常见内存泄露场景
静态变量与内存泄露
Java
中静态变量在类加载时初始化,并存储在方法区是类变量,在类卸载的时候销毁并释放清空。下面简单介绍下类加载和卸载,参考java 静态变量及类的生命周期 。
类加载
遇到 new, getstatic, putstatic, invokestatic
这 4 条字节码指令时、反射调用时、子类初始化时、虚拟机启动 main
主类时等等,会触发类加载并初始化。
静态变量
虚拟机在加载类的过程中为静态变量分配内存,static
变量在内存中只有一个,存放在方法区,属于类变量,被所有实例所共享。
类卸载
方法区会回收两部分内容:废弃常量和无用的类。类被回收时才会卸载,但是方法区回收类时条件比较严格,只有同时满足以下三个条件,才会回收:
- 该类所有的实例都已经被回收(
GC
),也就是虚拟机中不存在该Class
的任何实例 - 加载该类的
ClassLoader
已经被回收(GC
) - 该类的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问改类的方法
静态变量在类没有被销毁,也没有置null
的情况下,是不会被回收的(GC
)。
安全可靠性
Android
在资源不足的时候会杀掉一些进程,在资源足够时重启被杀掉的进程,这时可能会存在某些应用内存不足被杀后重启了。这会导致 Android
中的静态变量并不可靠,上次保存的静态数据可能没了,所以针对静态变量,必须保存一份到本地文件。另外,静态变量在进程退出时才会被销毁,所以在多账号的应用中,退出账号时上次保存的数据还在,需要手动重置那些与账户有关的静态数据,以免影响到另一个账户。
结论
静态变量只有在类卸载(条件很苛刻),虚拟机关闭,进程被杀等情况下,才会被回收。通常我们可以简单理解,Android
的静态变量和整个应用的生命周期相同,所以静态变量引用了当前类中的变量,会引起当前类对象无法被及时销毁,导致内存泄露。
静态变量要尽量少用,在用完后必须置空,养成好习惯。
非静态内部类与内存泄露
在 Java
中,非静态内部类都会持有外部类的引用,参考Java 内部类 。
通过反编译非静态内部类对应的 class
文件,可以看出内部类在构造方法中,传入了外部类的引用 this$0
,内部类在访问外部类的成员或方法时,都需要传递该参数 this.this$0
,只有静态内部类例外。这也可看出为什么静态内部类在实例化时不需要外部类实例化,而其他内部类在实例化时必须先实例化外部类了。
内部类虽然和外部类写在同一个文件中,但是编译完成后会生成各自的 class
文件,编译过程中:
- 编译器自动为非静态内部类添加一个成员变量,这个成员变量的类型和外部类的类型相同,这个成员变量就是指向外部类对象的引用
- 编译器自动为非静态内部类的构造方法添加一个参数,参数的类型是外部类的类型,这个参数为内部类中添加的成员变量赋值
- 在调用非静态内部类的构造函数初始化内部类对象时,会默认传入外部类的引用
换句话说:静态内部类不持有外部类对象的引用,而其他内部类都会持有。在 Android
中经常会使用成员内部类或者匿名内部类,特别是这些类开后台线程或者做耗时操作时,会引起外部类在退出时(仍然被非静态内部类持有引用)无法被释放,导致内存泄露。当然这种后台因为耗时任务引起的内存泄露,在耗时任务执行完后,会释放外部类的引用,从而再下次 GC
时,外部类可以被正常回收。如果后台任务为无限循环,则外部类会被持续持有。
尽量使用静态内部类,避免持有外部类的引用。
强弱软引用与内存泄露
为了规避内存泄露,通常使用软引用或弱引用。参考:JVM 内存分配及垃圾回收-对象引用的四种分类
注册和取消注册
监听系统服务,通常要将自己 Context
注册到系统中,这会导致服务持有了 Context
的引用,如果在 Activity
销毁的时没有注销这些监听器,会导致内存泄漏。所以注册和取消注册一定是成对出现的:
1 | registerListener(); |
Bitmap
使用不当造成内存泄露和溢出
Bitmap
非常容易导致内存溢出,通常 1200 万像素的手机拍下来的照片为 4048x3036
像素,如果默认配置为 ARGB_8888
(4 个字节存储),打开一张这样的图片大概需要 4048*3036*4/1024=48M
大小的内存,很容易导致应用内存溢出。所以 Bitmap
使用时需要非常小心并及时回收。参考 Android
官方文档:Handling Bitmaps ,Loading Large Bitmaps Efficiently ,Caching Bitmaps ,Managing Bitmap Memory 。
其他可能引起的内存泄露
- 资源性对象未关闭
如Cursor, File, Socket
等的使用,最后需要close
并置空。 - 屏幕旋转导致的
Activity
重建
反复旋转设备经常会导致应用泄漏Activity, Context, View
对象,因为系统会重新创建Activity
,而如果在其他地方保持对这些对象之一的引用,系统将无法对其进行垃圾回收。
常见内存泄露示例
内存泄露的根本原因:长生命周期对象引用了短生命周期对象导致。当短生命周期对象结束后,而长生命周期仍然持有这个引用,导致短生命周期对象无法被释放。
静态变量
根据上面分析,静态变量很容易引起内存泄露,应该尽量少用,用完后退出必须置空。如下为静态 Activity
变量和静态 View
引起的内存泄露示例。
错误示例
1 | private static Activity sLeakActivity; |
示例很简单,定义了两个静态变量分别保存当前 Activity
变量和某个 View
变量,而静态变量生命周期基本和应用生命周期相同,所以当前 Activity
退出时,因为静态变量持有它的引用,导致 Activity
实例无法被回收出现内存泄露。
正确示例
1 | // 1. 使用非静态变量保存 |
解决方法也很简单,不使用静态变量,或者在 Activity.onDestroy
时,将静态变量置空,断开引用链关系。
单例模式
在 Android
中,单例的静态特性使得单例的生命周期和应用的生命周期一样长,而单例中引用了 Activity
对象。当该 Activity
退出时,因为单例还持有这个对象,导致该 Activity
无法被回收,导致内存泄露。
错误示例
1 | public class MemLeakSample extends AppCompatActivity { |
在这个示例中,MemLeakSample
中调用了单例 SingletonLeak
,并将自身传递给单例,紧接着马上关闭退出。正常情况下 MemLeakSample
退出后,应当释放并回收,但是因为静态单例持有了 Context
的引用,导致其无法被回收,引起内存泄露。
正确示例
1 | // 1. Singleton |
Context
赋值为整个应用的上下文 this.context = context.getApplicationContext();
,这样单例 Context
就和应用的生命周期相同了,和具体的 Activity
无关,所以 MemLeakSample
退出后,直接释放并回收。
匿名内部类 - Thread/Runnable
后台耗时任务的匿名内部类,可能出现内存泄露。
错误示例
1 | private void anonymousThreadLeak(){ |
匿名 Runnable
在后台执行耗时任务,当前 Activity
退出,因为 Runnable
持有 Activity
的引用,导致出现内存泄露。但是当 Runnable
执行完后,会释放引用,下次 GC
时,Activity
能被正常回收。当然在编码时,更希望不要出现泄露的可能性。
正确示例
1 | // 使用静态内部类,避免持有外部类引用 |
使用静态内部类,避免持有外部类引用,从根本上避免内存泄露。和 Thread/Runnable
类似的还有 AsyncTask
,如果异步任务没有执行完而 Activity
退出,就会导致内存泄露。
匿名内部类 - Handler
所有 Message
都持有 Handler
的引用,而匿名内部类 Handler
会持有外部 Activity
,形成引用链。
错误示例
1 | private Handler mLeakHandler = new Handler(){ |
匿名 Handler
在处理延时消息这段时间时,如果 Activity
退出,而匿名 Handler
持有它的引用,导致 Activity
无法被正常释放,引起内存泄露。
正确示例
1 | public static class SafeHandler extends Handler{ |
使用静态内部类继承 Handler
,避免持有外部 Activity
的引用。但是 Handler
通常来更新 UI
,如果使用静态内部类,则无法正常访问 Activity
的 UI
控件了,这里采用弱引用的方式,保存 Activity
的实例来更新 UI
,确保在 GC
时,Activity
能被正常回收。
注册系统服务监听
通过 Context.getSystemService(int name)
获取系统服务,这些服务工作在各自的进程中,如果需要使用这些服务,可以注册监听器,这会导致服务持有了 Context
的引用,如果在 Activity
销毁的时没有注销这些监听器,会导致内存泄漏。
错误示例
1 | private SensorManager mSensorManager; |
正确示例
1 | private void safeRegister(){ |
注册后,需要在 Activity.onDestroy
中取消注册,避免系统服务持有当前 Activity
的引用,从而规避内存泄露。
内存泄露检测工具
Android Profiler['proʊfaɪlə(r)]
是 Android Studio 3.0
推出的一个监控工具,分为三大模块:CPU
、内存 、网络。利用 Memory Profiler
来监控并分析当前应用内存的使用情况,官网地址:Android Profiler Memory Profiler 。Memory Profiler
可以识别导致应用卡顿、冻结甚至崩溃的内存泄漏和流失。能够显示应用内存使用量的实时图表,捕获堆转储、强制执行垃圾回收以及跟踪内存分配等功能。
简介
打开方式:点击 View > Tool Windows > Android Profiler
,或者点击工具栏中的 Android Profiler
图标打开 Android Profiler
。然后点击 MEMORY
时间线中的任意位置可打开 Memory Profiler
。
图片中 1-7
按钮分别表示:
- 用于强制执行垃圾回收的按钮(
GC
)。 - 用于捕获堆转储的按钮。
- 用于记录内存分配情况的按钮。此按钮仅在连接至运行
Android 7.1
或更低版本的设备时才会显示。 - 用于放大/缩小时间线的按钮。
- 用于跳转至实时内存数据的按钮。
Event
时间线,其显示Activity
状态、用户输入Event
和屏幕旋转Event
。- 内存使用量时间线,其包含以下内容:
- 一个显示每个内存类别使用多少内存的堆叠图表,如左侧的
y
轴以及顶部的彩色键所示。 - 虚线表示分配的对象数,如右侧的
y
轴所示。 - 用于表示每个垃圾回收
Event
的图标。
如果遇到:Advanced profiling is unabailable for the selected process
这个问题,是因为在 Android 7.1
或更低版本的设备需要开启 Profiler
配置,Android 8.0
及以上不会存在。
解决方案:Run -- Edit Configurations...
打开应用配置界面,选择应用并在右边的 tab
中勾选 Enable advanced profiling
。
查看内存泄露
点击 Memory Profiler
的堆转储按钮,捕获应用中象使用内存的当前状态。特别是在长时间的用户会话后,堆转储会显示那些不应再位于内存中却仍在内存中的对象,从而帮助识别内存泄漏。堆转储中可以看到:
- 应用已分配哪些类型的对象,以及每个类型分配多少
- 每个对象正在使用多少内存
- 在代码中的何处仍在引用每个对象
在 Instance View
中,每个实例都包含以下信息:
Depth
从任意GC
根到所选实例的最短hop
数,如果不为 0 ,则表示无法回收(内存泄露可疑点)。Shallow Size
:此实例的大小Retained Size
:此实例支配的内存大小
从
Memory Profiler
中只能看出内存泄露产生了,主要是按包查看指定应用的实例引用数(Depth
)是否为 0 。如果不为 0 ,基本可以确定存在内存泄露了,需要查看代码慢慢检查。如果为 0,下次GC
时会回收这些对象。
分析 hprof
文件
HPROF
文件是一种二进制堆转储格式文件,包含了内存相关信息,可以直接使用 AS
打开这类文件。
从图中可以看出,打开右上角的 Analyzer Tasks
页,点击开始按钮后,自动分析 hprof
文件中 Activity/String
的内存泄露,通常我们用来看 Activity
的内存泄露。点击泄露的 Activity
后,在左下角能看到调用关系,可以很明确的得出具体是哪个变量引用没有释放。
参考网页:HPROF文件查看和分析工具
其他检查工具
Leak Canary
开源内存泄露检测库官网 ,能跟踪到内存泄露的代码具体位置,非常好用,内存泄露时会给出弹框提示。MAT[Memory AnalysisTools]
Eclipse
推出的内存泄露分析工具官网下载地址 ,可以独立运行。网上有相关分析介绍腾讯Bugly-Android内存泄漏MAT分析 。dumpsys meminfo
命令
命令格式:adb shell dumpsys meminfo package_name|pid [-d]
。 [Android 官网指南] https://developer.android.google.cn/studio/command-line/dumpsys.html ,memory
参考文档
- 深入理解
Java
虚拟机:JVM
高级特性与最佳实践 第 2 版 - Android内存泄漏查找和解决
- 腾讯Bugly-Android内存泄漏的简单检查与分析方法
- 腾讯Bugly-内存泄露从入门到精通三部曲之基础知识篇
- 腾讯Bugly-内存泄露从入门到精通三部曲之排查方法篇
- 腾讯Bugly-内存泄露从入门到精通三部曲之常见原因与用户实践
- 单例模式造成的内存泄漏
- Memory Profiler
- Android Profiler内存泄漏检查
- Android Profiler分析器
- github LeakCanary
- 腾讯Bugly-Android内存泄漏MAT分析
- HPROF文件查看和分析工具
- java 静态变量及类的生命周期
- 深入理解Java中为什么内部类可以访问外部类的成员
- Android内存泄漏的八种可能-上
- Android内存泄漏的八种可能-下
- Android 内存泄漏总结
- 内存泄露实例分析