Android内存泄露总结

 

引言:JAVA和C++之间有一堵由内存分配和垃圾收集技术所围成的高墙,墙外的人想进来,墙里面的人想出来。...



引言:
JAVA和C++之间有一堵由内存分配和垃圾收集技术所围成的高墙,墙外的人想进来,墙里面的人想出来。
对于从事C 和C++ 程序开发的开发人员来说,在内存管理领域,他们既是拥有最高权力的皇帝,又是从事最基础工作的劳动人民;
既拥有每一个对象的所有权,又担负着每一个对象生命开始到终结的维护责任。
对于Java 程序员来说; 在虚拟机的自动内存管理机制的帮助下,不再需要为每一个new 操作去写配对的delete/free 代码;
而且不容易出现内存泄漏和内存溢出问题看起来由虚拟机管理内存一切都很美好;
不过,也正是因为Java 程序员把内存控制的权力交给了Java 虚拟机,
一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的那排查错误将会成为一项异常艰难的工作。
——周志明《深入理解Java虚拟机》*

Android 内存泄漏总结

内存管理的目的就是让我们在coding中怎么有效的避免我们的应用出现内存泄漏的问题,提高应用的体验和质量。内存泄漏是指,该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收。
Android使用的是java语言,首先我们来了解一下java的内存泄露的原理。

一、Java 内存分配策略

Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,
对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。

静态存储区(方法区):
主要存放静态数据、全局 static 数据和常量。
这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

栈区 :
当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。
因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

堆区 :
又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。
这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

栈与堆的区别:

在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。
当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,
该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。
在堆中分配的内存,将由 Java 垃圾回收器来自动管理。
在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。
我们可以通过这个引用变量来访问堆中的对象或者数组。

举个例子:



Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。
mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。

结论:

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。
—— 因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体)
—— 因为它们属于类,类对象终究是要被new出来使用的。

了解了 Java 的内存分配之后,我们再来看看 Java 是怎么管理内存的。

二、Java是如何管理内存

Java的内存管理就是对象的分配和释放问题。
在Java中,程序员需要通过关键字new为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。
另外,对象的释放是由GC决定和执行的。
在Java中,内存的分配是由程序完成的,而内存的释放是有GC完成的,这种收支两条线的方法确实简化了程序员的工作。
但同时,它也加重了JVM的工作;这也是Java程序运行速度较慢的原因之一。
因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。

监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。
另外,每个线程对象可以作为一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。
在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。
如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。
以下,我们举一个例子说明如何用有向图表示内存管理。
对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。
以下右图,就是左边程序运行到第6行的示意图。



Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。
这种方式的优点是管理内存的精度很高,但是效率较低。
另外一种常用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。

三、什么是Java中的内存泄露

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;
其次,这些对象是无用的,即程序以后不会再使用这些对象。
如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

在C++中,内存泄漏的范围更大一些。
有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。
在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。
通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序员只需要管理边就可以了(不需要管理顶点的释放)。
通过这种方式,Java提高了编程的效率。



因此,通过以上分析,我们知道在Java中也有内存泄漏,但范围比C++要小一些。
因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。

对于程序员来说,GC基本是透明的,不可见的。
虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义, 该函数不保证JVM的垃圾收集器一定会执行。
因为,不同的JVM实现者可能使用不同的算法管理GC。
通常,GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。
但通常来说,我们不需要关心这些,除非在一些特定的场合,GC的执行影响应用程序的性能。
例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,
那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,
例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。

同样给出一个 Java 内存泄漏的典型例子,



在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,
那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。
因此,如果对象加入到Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。

四、Android中常见的内存泄漏汇总

1、单例造成的内存泄漏

单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏,由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。
如下这个典例:



这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:

1、 传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长

2、 传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),
所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。

所以正确的单例应该修改为下面这种方式:



这样不管传入什么Context最终将使用Application的Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。

2、非静态内部类创建静态实例造成的内存泄漏

有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:



这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,
而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。

正确的做法为:
将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application 的 Context。
当然,Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context;
对于Application,Service,Activity三者的Context的应用场景如下:



3、Handler造成的内存泄漏

Handler的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler来处理,
对于Handler的使用代码编写一不规范即有可能造成内存泄漏,如下示例:



在该 SampleActivity 中声明了一个延迟10分钟执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。
当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,
所以 此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

修复方法:
在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。
同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,
这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,
所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息,更准确的做法如下:



4、线程造成的内存泄漏

对于线程造成的内存泄漏,也是平时比较常见的,如下这两个示例可能每个人都这样写过:



上面的异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。
如果Activity在销毁之前,任务还未完成,那么将导致Activity的内存资源无法回收,造成内存泄漏。
正确的做法还是使用静态内部类的方式,如下:

这样就避免了Activity的内存资源泄漏,当然在Activity销毁时候也应该取消相应的任务AsyncTask::cancel(),避免任务在后台执行浪费资源。

5、资源未关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,
应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

一些建议:
1、对 Activity 等组件的引用应该控制在 Activity 的生命周期之内;
如果不能就考虑使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部长生命周期的对象引用而泄露。

2、尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。

3、对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:
a)将内部类改为静态内部类
b)静态内部类中使用弱引用来引用外部类的成员变量

4、Handler 的持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。
比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message和 Runnable.

5、在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap 后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ; array = null)等,最好遵循谁创建谁释放的原则。

6、正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。

7、保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。


    关注 应用软件所Family


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册