参考资料:
Android 内存泄漏总结
Android内存泄漏小谈
你真的懂 Java 的内存管理和引用类型吗?
Android 性能优化:手把手带你全面了解内存泄露
[贝聊科技]使用Android Studio和MAT进行内存泄漏分析
【Android 性能优化】—— 详解内存优化的来龙去脉
Android 关于内存泄露,你必须了解的东西
Android性能优化系列之内存优化
Android 内存泄漏案例和解析
Android 内存泄漏分析心得
Android开发常见的Activity中内存泄漏及解决办法
Android内存泄漏查找和解决
Android 内存泄漏全解
彻底搞懂Java内存泄漏
简析Android的垃圾回收与内存泄露
Android应用内存泄漏的定位、分析与解决策略
Android 性能优化 - 彻底解决内存泄漏
利用Android Studio、MAT对Android进行内存泄漏检测
Android内存泄漏学习笔记
内存泄漏是Android中一个很常见的问题, 所以我决定尽可能全面的总结一下内存泄漏。因为篇幅过长,分两篇。第一篇总结常见的内存泄漏和解决方案,第二篇介绍常用的内存泄漏的检测方法和工具。
Java内存分配策略
Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。
- 静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
- 栈区:方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。
- 堆区: 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。
堆与栈的区别
在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。
堆内存用来存放所有由new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。
举个例子
1 | public class Sample { |
Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。
mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。
结论:
局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。—— 因为它们属于方法中的变量,生命周期随方法而结束。
成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)—— 因为它们属于类,类对象终究是要被new出来使用的。
Java的四种引用
- 强引用(Strong Reference): 指在程序代码之中普遍存在的,类似 Object obj = new Object() 这类的引用,只要强引用还存在,垃圾回收器「永远」不会回收掉被引用的对象
- 软引用(Soft Reference): 用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常
- 弱引用(Weak Reference): 用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
- 虚引用(Phantom Reference): 也被称为幽灵引用或幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。
用一张表来总结java的四种引用
类型 | 回收时机 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 在内存不足时 | 联合ReferenceQueue构造/有效期短/占内存大/生命周期长的对象的二级高速缓存器(内存不足时才清空) | 内存不足时终止 |
弱引用 | 在垃圾回收时 | 联合ReferenceQueue构造/有效期短/占内存大/生命周期长的对象的一级高速缓存器(系统发生gc时则清空) | gc运行后终止 |
虚引用 | 在垃圾回收时 | 联合ReferenceQueue来跟踪对象被垃圾回收器回收的活动 | gc运行后终止 |
代码测试一下
1 | public class Main { |
运行结果
1 | 软引用: |
什么是内存泄漏
简单来说就是当你不再需要某个实例后,但是这个对象却仍然被引用,导致系统无法进行回收, 这个情况就叫做内存泄漏(Memory Leak)。
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常GC采用有向图的方式记录并管理堆中的所有对象,通过这种方式确定哪些对象时“可达”,哪些对象时“不可达”。当对象不可达的时候,即对象不再被引用的时候,就会被垃圾回收。
举个例子:
在第六行的时候,o2改变了指向,Obj2就不再引用main的了,即他它们是不可达的,Obj2就可能在下次的GC中被回收
内存泄漏的原因
出现内存泄露的原因仅仅是外部人为原因 = 无意识地持有对象引用,使得 持有引用者的生命周期 > 被引用者的生命周期
泄漏的源头
这里暂时分为三类
- 自身编码引起
由项目开发人员自身的编码造成。 - 第三方代码引起
这里的第三方代码包含两类:第三方非开源的SDK和开源的第三方框架。 - 系统原因
由 Android 系统自身造成的泄漏,如像 WebView 、 InputMethodManager 等引起的问题,还有某些第三方 ROM 存在的问题。
常见的内存泄漏
下面举例说明常见的内存泄漏
静态变量造成的内存泄漏
静态变量的生命周期,起始于类的加载,终止于类的释放。对于 Android 而言,程序也是从一个 main 方法进入,开始了主线程的工作,如果一个类在主线程或旁枝中被使用到,它就会被加载,反过来说,假如一个类存在于我们的项目中,但它从未被我们使用过,算是个孤岛,这时它是没有被加载的。一旦被加载,只有等到我们的 Android 应用进程结束它才会被卸载。
举个例子:
1 | public class LeakStaticActivity extends AppCompatActivity { |
看一下LeakCanary检测出来的内存泄漏
这样的代码会导致当这个 Activity 结束的时候,sContext 仍然持有它的引用,致使 Activity 无法回收。同样的,如果一个 Activity 的静态 field 变量内部(比如一个静态的View)获得了当前 Activity 的引用,比如我们经常会把 this 传给 View 之类的对象,这个对象若是静态的,并且没有在 Activity 生命周期结束之前置空的话,也会导致同样的问题。
看一下静态View造成的内存泄漏:
1 | public class StaticViewLeakActivity extends AppCompatActivity { |
View一旦被加载到界面中将会持有一个Context对象的引用,在这个例子中,这个context对象是我们的Activity,声明一个静态变量引用这个View,也就引用了activity,所以当activity生命周期结束了,静态View没有清除掉,还持有activity的引用,因此内存泄漏了。
解决办法:
- 在这个 Activity 的 onDestroy 时将 sContext 的值置空
- 避免使用静态变量这样的写法()
- 尽量避免 Static 成员变量引用资源耗费过多的实例(如 Context), 若需引用 Context,则尽量使用Applicaiton的Context
- 使用弱引用代替强引用。
单例造成的泄漏
单例造成的原因其实和上面的静态变量一样
举个例子:
1 | public class AppManager { |
可以看到AppManager这个对象中的变量mContext引用着SingletonLeakActivity这个对象, 然后这个AppManager对象又是一个静态的的类变量, 生命周期和等于应用的生命周期, 导致activityj结束生命周期需要被销毁时而无法被回收。
解决办法:
1 | private AppManager(Context context) { |
非静态内部类和匿名内部类造成内存泄漏
handler造成的内存泄漏
最为常见的就是非静态的handler造成的内存泄漏了。先看看原因。
在 Java 中,非静态内部类包括匿名内部类,比如非静态的Handler会引用外部类对象this(比如 Activity)
而message又会引用handler, 看一下Handler的代码
1 | private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { |
可以看到Message有一个target字段引用着handler。
假如Handler发送的Message尚未被处理(比如一个延时执行的Message),就会一直存在于对应线程的MessageQueue中。在这段时间内,该Message会被MessageQueue一直持有,而Message又引用着Handler, Handler又引用着Activity,导致Activity在finish的时候无法被回收。
解决方案:
1 | public class HandlerLeakActivity extends AppCompatActivity { |
虽然我们解决了Activity的内存泄漏问题,但是经过Handler发送的延时消息还在MessageQueue中,Looper也在等待处理消息,所以我们要在Activity销毁的时候处理掉队列中的消息。
1 |
|
非静态内部类/匿名类
先看看非静态内部类(non static inner class)和 静态内部类(static inner class)之间的区别
类型 | 与外部class引用关系 | 被调用时是否需要外部实例 | 能否调用外部class 中的变量和方法 | 生命周期 |
---|---|---|---|---|
静态内部类 | 如果没有传入参数,就没有引用关系 | 不需要 | 不能 | 自主的生命周期 |
非静态内部类 | 自动获得强引用 | 需要 | 能 | 依赖于外部类,甚至比外部类更长 |
可以看到非静态内部类自动获得外部类的强引用,而且它的生命周期甚至比外部类更长,这便埋下了内存泄露的隐患。如果一个 Activity 的非静态内部类的生命周期比 Activity 更长,那么 Activity 的内存便无法被回收,也就是发生了内存泄露,而且还有可能发生难以预防的空指针问题。
举个例子:
1 | public class AsyncTaskActivity extends AppCompatActivity { |
可以看到我们在 Activity 中继承 AsyncTask 自定义了一个非静态内部类,在 doInbackground() 方法中做了耗时的操作,然后在 onCreate() 中启动 MyAsyncTask。如果在耗时操作结束之前,Activity 被销毁了,这个线程在Activity销毁后还一直在后台执行,那这个线程会继续持有这个Activity的引用从而不会被GC回收,直到线程执行完成,这时候便会产生内存泄露。
解决方案1:使用非静态的内部类
1 | public class MainActivity extends AppCompatActivity { |
解决方案2:
当外部类结束生命周期时,强制结束线程。例如AsyncTask.cancel(), Thread.stop()或者Thread.interrupt()(但是一般建议最好不要这么做,有时会有bug)。
集合类
集合类添加元素后,仍引用着集合元素对象,导致该集合元素对象不可被回收,从而导致内存泄漏
举个例子:
1 | static List<Object> objectList = new ArrayList<>(); |
在这个例子中,循环多次将 new 出来的对象放入一个静态的集合中,因为静态变量的生命周期和应用程序一致,而且他们所引用的对象 Object 也不能释放,这样便造成了内存泄露。
解决方法:
在集合元素使用之后从集合中删除,等所有元素都使用完之后,将集合置空。
1 | objectList.clear(); |
WebView 的泄漏
Android 中的 WebView 存在很大的兼容性问题。WebView 解析网页时会申请 Native 堆内存用于保存页面元素,当页面较复杂时会有很大的内存占用。如果页面包含图片,内存占用会更严重。并且打开新页面时,为了能快速回退,之前页面占用的内存也不会释放。有时浏览十几个网页,都会占用几百兆的内存。这样加载网页较多时,会导致系统不堪重负,最终强制关闭应用,也就是出现应用闪退或重启。
由于占用的都是 Native 堆内存,所以实际占用的内存大小不会显示在常用的 DDMS Heap 工具中(这里看到的只是 Java 虚拟机分配的内存,一般即使 Native 堆内存已经占用了几百兆,这里显示的还只是几兆或十几兆)。只有使用 adb shell 中的一些命令比如 dumpsys meminfo 包名,或者在程序中使用 Debug.getNativeHeapSize() 才能看到。
据说由于 WebView 的一个 BUG,即使它所在的 Activity(或者 Service) 结束也就是 onDestroy() 之后,或者直接调用 WebView.destroy() 之后,它所占用这些内存也不会被释放。
解决这个问题最直接的方法是:把使用了 WebView 的 Activity(或者 Service) 放在单独的进程里,通过 AIDL 与主进程进行通信。然后在检测到应用占用内存过大有可能被系统干掉,或者它所在的 Activity(或者 Service) 结束后,调用 System.exit(0),主动 Kill 掉进程。或者可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。由于系统的内存分配是以进程为准的,进程关闭后,系统会自动回收所有内存。
关于WebView的更多内容,请参见:Android WebView Memory Leak WebView 内存泄漏
不需要用的监听未移除产生的内存泄漏
例子1:
1 | tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() { |
例子2:
1 | SensorManager sensorManager = getSystemService(SENSOR_SERVICE); |
属性动画造成内存泄漏
另外当我们使用属性动画,我们需要调用一些方法将动画停止,特别是无限循环的动画,否则也会造成内存泄漏,好在使用 View 动画并不会出现内存泄漏,估计 View 内部有进行释放和停止。
RxJava 使用不当造成内存泄漏
对于异步的操作,如果没有及时取消订阅,就会造成内存泄漏:
1 | Observable.interval(1, TimeUnit.SECONDS) |
同样是匿名内部类造成的引用没法被释放,使得如果在 Activity 中使用就会导致它无法被回收,即使我们的 Action1 看起来什么也没有做。
解决方案:接收 subscribe 返回的 Subscription 对象,在 Activity onDestroy 的时候将其取消订阅
1 | public class LeakActivity extends AppCompatActivity { |
资源未关闭造成的内存泄漏
对于使用了BraodcastReceiver,ContentObserver,File, Cursor,Stream,Bitmap, EventBus等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
下期:总结一些常用的内存泄漏的检测方案