如何定位和解决linux 内存泄露 定位

Android内存泄露案例分析
发表于 11:02|
作者张权威
摘要:一款优秀的Android应用,不仅要有完善的功能,也要有良好的体验,而性能是影响体验的重要因素之一。内存泄露是Android开发中常见的性能问题。本文作者以真实案例演示内存泄露从发现、分析定位到最终解决的全过程。
CSDN移动将持续为您优选移动开发的精华内容,共同探讨移动开发的技术热点话题,涵盖移动应用、开发工具、移动游戏及引擎、智能硬件、物联网等方方面面。如果您想投稿、参与内容翻译工作,或寻求近匠报道,请发送邮件至tangxy#csdn.net(请把#改成@)。&
本文整理自:【】(点击链接,观看视频),演讲PPT&&。一款优秀的Android应用,不仅要有完善的功能,也要有良好的体验,而性能是影响体验的一个重要因素。内存泄露是Android开发中常见的性能问题。这篇文章,通过我们曾经遇到的一个真实的案例,来讲述一个内存泄露问题,从发现到分析定位,再到最终解决的全过程。
这里把整个过程分为四个阶段:
第一阶段,现场勘查,分析Bug现象,找出有用线索;
第二阶段,初步推断,根据之前的线索,推断可能导致Bug的原因,并且进一步验证推断是否正确;
第三阶段,探究根源,找出导致Bug的真正原因;
第四阶段,解决方案,研究如何解决问题。
之前我们开发过一款应用,交给QA测试之后,发现有时候界面会卡顿,动画不流畅。经过他反复测试找到了规律,当连续多次打开应用时,问题就会出现。我们根据这个方式重现Bug时,又发现Logcat中频繁输出GC日志(如图一所示)。&
这里先简单介绍一下GC,也就是垃圾回收机制,Android通过提供垃圾回收机制来管理内存,当内存不足时会触发垃圾回收,回收没用的对象,释放内存。我们通过两张图(图二、三)来看一下垃圾回收的过程。
这里GC Roots表示垃圾回收器对象,每个节点表示内存中的对象,箭头表示对象之间的引用关系,能被GC Roots直接或者间接引用到的对象ABCD,表示正在使用的对象,不能被引用到的EFG是无用对象,垃圾回收时就会被回收掉。当系统触发一次垃圾回收时,对象EFG就会被回收。以上就是垃圾回收的过程。在现场勘查这一阶段,我们找到两条非常有用的线索:
线索一:连续多次打开应用之后,界面卡顿,动画不流畅;
线索二:操作过程中,LogCat频繁输出GC日志。
现在到第二阶段,根据前一阶段找到的线索,当连续多次打开应用,界面卡顿,同时Logcat不断输出GC日志,初步推测我们的应用中存在内存泄漏。首先我们先看一下什么是内存泄露呢?我们通过两张张图来演示,如图四和五。
这张图跟刚才演示GC过程的图很像,这时候再触发GC时,EG会被回收,F对于应用来说虽然无用了,却无法被回收,最后导致了内存泄漏。
因此,很可能每次打开应用时,都会产生像F这样的对象导致,内存占用越来越高,系统频繁触发GC。
接下来就要去验证,这里我们利用DDMS工具。
DDMS是虚拟机调试监控服务,它能帮助我们测试设备截屏,设置虚拟地理坐标,针对特定的进程查看它的堆信息等等。
如何利用它来验证我们的推断呢?首先要load出应用的内存快照,这里分为4步,第一步,选中我们要查看的应用,第二步点击Update Heap按钮,这时候DDMS就会通知应用准备收集内存信息,第三步选择Heap标签,heap标签页能够展示出内存的所有信息。第四步点击Cause
GC,这时候就会把内存快照load出来。这样DDMS就把内存快照load出来了。具体操作如图六。&
Load出内存信息之后,就来分析我们应用中是否存在内存泄漏,分析内存泄漏的关键的数据之一,就是Total Size。
重复打开应用时,如果不存在内存泄漏的话,Total Size只会在一定范围内波动。如果我们的推断正确,连续打开应用,Total Size会持续增加。接着我们就来测试分析,连续打开应用,如图七。
这里展示了第一二三,以及第十次打开时Total size的截图,Total Size一直在增大,其中1-byte array增大最为明显,1-byte
array表示的是byte[],或者boolean[]类型的数组。所以我们能够得出结论:打开应用时,确实存在内存泄漏。
确认了问题,接下来就要探究问题的根源。每一个应用运行过程中,都会持有上万甚至百万个对象,我们就要分析这些对象在内存中的状态,看哪些对象对应用来说已经没用了,但是还在占用着内存。
这个过程我们用到了MAT。MAT是一款功能丰富,运行速度非常快的堆内存分析工具。它能够快速的分析堆中的所有对象,计算出每个对象占有的内存大小。它的功能非常强大,分析完内存之后,它还能够帮找出可能导致内存泄漏的对象,列出占用内存比较大的对象,它提供查询java容器对象使用率的等功能,这些功能对于我们分析应用的内存都非常有帮助。
它既有独立的安装程序,也有针对eclipse的插件,我们根据自己的需求下载相应的程序。我们使用的时候也非常简单,可以利用刚才介绍的DDMS工具,把内存快照导出到.hprof文件中,然后MAT直接打开这个hprof文件就行了。独立安装程序的下载地址:
根据前面的测试,我们经过几次操作就导致1-byte array的Total Size从20M增大到70M,平均每次增加5M左右,这个size是比较大的,因此推断有内存占用比较大的对象导致的内存泄露。结合MAT的Dominator
Tree功能,我们来着手分析,Dominator Tree能列出内存中所有对象,以及他们占用内存的大小。
这里是Dominator Tree的一张截图,先介绍两个名词第一个Shallow Heap,表示对象本身的内存大小,包括对象的头以及成员变量等,第二个Retained
Heap表示:一个对象本身以及它持有的所有对象的内存总和,也就是GC时,回收一个对象所释放的所有内存空间。从这张图中可以看到,Retained Heap最大的时Resources对象,但是Resource是System Class对象,也就是系统管理的对象,也不会是引起我们内存泄漏的原因,我们不用去分析它。
第二大的就是Bitmap对象。从前面的介绍我们已经知道,如果一个对象能被GC Roots直接或者间接引用,它就不能被回收,那我们就来看一下Bitmap到GC Roots的引用路径,看Bitmap时被哪个对象持有的。选中Bitmap,右键选择,Path To GC Roots,再选择execlude weak references,因为弱引用是不能阻止垃圾回收的,所以我们直接排除弱引用。
下面图九就是Bitmap到GC Roots的引用路径。其中LoadPicThread对象前面有个小红点,这个小红点就表示这个对象是被GC Roots直接持有的。
所以整个引用路径就是GC Roots引用着Thread,Thread引用着我们的Activity,而Activity中包含了Bitmap对象。
这时候当前界面已经退出了,但是Thread 仍持有着Activity 的引用,导致Activity 和它引用的内存例如Bitmap不能被回收。这时候问题的真相基本浮出水面了。
为了进一步确认我们的结果,我们从另一个角度进行验证,看内存中是否多个被Thread持有的,不能回收的Activity的对象?借助MAT的Histogram功能,它能列出内存中的所有类,以及每个类的实例个数。
如图十,MAT提供了正则搜索的功能,可以根据类名搜索,我们这里搜索得到的结果是11个Activity对象,所以进一步验证成功。就是因为我们创建的那个Thread持有着Acitivy的对象,导致关闭之后Activiy不能回收。
根据以上的分析,我们找到了引发内存泄露的代码。
class TestActivity{
protected void onCreate(Bundle savedInstanceState) {
LoadPicThread loadPicThread = new LoadPicThread();
loadPicThread.start();
private class LoadPicThread extends Thread {
public void run() {
super.run();
while(true) {
…load data…
Thread.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
LoadPicThread是TestActivity的一个内部类,它隐式的持有着TestActivity的实例,LoadPicThread会每5分钟去服务器请求一次数据,这个Thread一直都不会结束,而且每次打开界面时都会创建一个这样的Thread。所以我们这里导致内存泄漏的根本原因就是长生命周期对象(Thread)持有短生命周期对象(Activity)的引用,导致Activiy退出之后,不能被回收。
最后到解决问题阶段,找到问题之后怎么解决呢?我们想到了两种解决方案。
第一,将Thread从Activity移除,可以放到后台服务中,这样Activity与Thread之间就不会相互依赖,如果Thread要做的事情跟Activity业务逻辑不是很紧密,例如在一些数据缓存的操作,这时候就可以用这种方案。
第二,当Activity结束时,停止Thead,让Thread与Activity的生命周期保持一致,一般可以在onDestory方法中,给Thread发送一个结束信号。
以上是就是我们从发现到解决内存泄漏的整个过程。其实在Android开发过程中,很多错误的代码,引发内存泄露。例如,不当的使用Context;构造Adapter时,没有使用缓存的convertView等等。
最后总结一下:
第一,作为Android开发人员,只有深刻理解Android常用组件的工作机制,以及应用中各个对象的生命周期,才能尽量避免写出导致内存泄露的代码;
第二,当程序出现问题时,首先要找到触发它的场景,就像这个案例中,我们根据QA提供的重现方式,经过反复测试和观察,最终定位到问题。而在我们日常开发中,可能遇到更加复杂的问题,在面对复杂的情况下,只有找到触发问题的关键场景,我们才能快速的定位问题,并加以解决。
第三,强大的工具是帮助我们分析和定位问题的利器,例如前面用到的DDMS和MAT工具,他们能够让我们能够深入到应用的内部进行探索和研究,从而快速的分析到问题的根源。所以开发人员应该学会运用这些强大的工具,来分析解决各种疑难问题。作者简介:张权威,资深Android开发工程师,现就职于琥珀天气,为琥珀天气主力开发人员,负责App架构设计与开发。
将于10月15-16日在北京新云南皇冠假日酒店召开。大会特设五大技术专场:平台与技术iOS、平台与技术Android、产品与设计、游戏开发、企业移动化、虚拟现实专场。此外,大会更是首次举办国内极具权威影响力的IoT技术峰会,特设硬件开发技术与嵌入式开发两大专场。大会将聚集国内最具实力的产品技术团队,与开发者一道进行最前沿的探讨与交流。&
第一时间掌握最新移动开发相关信息和技术,请关注mobilehub公众微信号(ID: mobilehub)。
推荐阅读相关主题:
CSDN官方微信
扫描二维码,向CSDN吐槽
微信号:CSDNnews
相关热门文章如何定位和解决Andorid的内存溢出问题(大总结)
我们经常在做项目过程中遇到内存溢出的问题,同时面试中关于OOM的问题也常常出现。
这里,我将前辈们解决Andorid内存溢出的方法重新整理一番,方便自己以后使用。最后附上参考博文。
一、Android的内存机制
android应用层是由java开发的,android的davlik虚拟机与jvm也类似,只不过它是基于寄存器的。在java中,通过new为对象分配内存,所有对象在java堆内分配空间;而内存的释放是由垃圾收集器(GC)来回收的。 Java采用了有向图的原理。Java将引用关系考虑为图的有向边,有向边从引用者指向引用对象。线程对象可以作为有向图的起始顶点,该图就是从起始顶点(GC roots)开始的一棵树,根顶点可以到达的对象都是有效对象,GC不会回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。
二、Android的内存溢出原因
1、内存泄露导致
由于我们程序的失误,长期保持某些资源(如Context)的引用,垃圾回收器就无法回收它,当然该对象占用的内存就无法被使用,这就造成内存泄露。
中常见就是Activity被引用在调用finish之后却没有释放,第二次打开activity又重新创建,这样的内存泄露不断的发生,则会导致内存的溢出。
Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。Android为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程. <span style="color:#、占用内存较多的对象
保存了多个耗用内存过大的对象(如Bitmap)或加载单个超大的图片,造成内存超出限制。
三、常见的内存泄漏问题及其解决方案
1、引用没释放造成的内存泄露
1.1注册没取消造成的内存泄露
&这种Android的内存泄露比纯Java的内存泄漏还要严重,因为其他一些Android程序可能引用系统的Android程序的对象(比如注册机制)。即使Android程序已经结束了,但是别的应用程序仍然还有对Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。
1.2集合中对象没清理造成的内存泄露
我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
1.3 static
&static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。
&&& private static ActivitymC&&&&&& //省略
如何才能有效的避免这种引用的发生呢?
&&& 第一,应该尽量避免static成员变量引用资源耗费过多的实例,比如Context。
&&& 第二、Context尽量使用ApplicationContext,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。
看使用的周期是否在activity周期内,如果超出,必须用application;常见的情景包括:AsyncTask,Thread,第三方库初始化等等。
还有些情景,只能用activity:比如,对话框,各种View,需要startActivity的等。
总之,尽可能使用Application。
&&& 第三、使用WeakReference代替强引用。比如可以使用WeakReference&Context&mContextR
1.4、线程(内部类的使用)
线程产生内存泄露的主要原因在于线程生命周期的不可控。如果我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。
如果非静态内部类的方法中,有生命周期大于其所在类的,那就有问题了。比如:AsyncTask、Handler,这两个类都是方便开发者执行异步任务的,但是,这两个都跳出了Activity/Fragment的生命周期。
&&& 第一、将线程的内部类,改为静态内部类。
&&&&&& 原因:
因为非静态内部类会自动持有一个所属类的实例,如果所属类的实例已经结束生命周期,但内部类的方法仍在执行,就会hold其主体(引用)。也就使主体不能被释放,亦即内存泄露。静态类编译后和非内部类是一样的,有自己独立的类名。不会悄悄引用所属类的实例,所以就不容易泄露。
&&& 第二、如果需要引用Acitivity,使用弱引用。
2、资源对象没关闭造成的内存泄露
资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。而不是等待GC来处理。它们的缓冲不仅存在于java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄露。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。而且android数据库中对Cursor资源的是又限制个数的,如果不及时close掉,会导致别的地方无法获得。
3、一些不良代码成内存压力
有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存,对内存的回收和分配造成很大影响的,容易迫使虚拟机不得不给该应用进程分配更多的内存,造成不必要的内存开支。
3.1 Bitmap没调用recycle()
Bitmap对象在不使用时,我们应该先调用recycle()释放内存,然后才设置为null.
虽然recycle()从源码上看,调用它应该能立即释放Bitmap的主要内存,但是测试结果显示它并没能立即释放内存。但是我猜应该还是能大大的加速Bitmap的主要内存的释放。
3.2 构造Adapter时,没有使用缓存的&convertView
&以构造ListView的BaseAdapter为例,在BaseAdapter中提共了方法:
public&View&getView(int&position,&View&convertView,&ViewGroup&parent)
来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list&item的view对象会被回收,然后被用来构造新出现的最下面的list&item。这个构造过程就是由getView()方法完成的,getView()的第二个形参&View&convertView就是被缓存起来的list&item的view对象(初始化时缓存中没有view对象则convertView是null)。
&&&&由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费时间,也造成内存垃圾,给垃圾回收增加压力,如果垃圾回收来不及的话,虚拟机将不得不给该应用进程分配更多的内存,造成不必要的内存开支。
四、占用内存较多的对象(图片过大)造成内存溢出及其解决方案
因为Bitmap占用的内存实在是太多了,特别是分辨率大的图片,如果要显示多张那问题就更显著了。Android分配给Bitmap的大小只有8M。
方法1:等比例缩小图片
有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一
尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图, 因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存。
因此,改用先通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的 source, decodeStream最大的秘密在于其直接调用JNI&&nativeDecodeAsset()来完成decode, 无需再使用java层的createBitmap,从而节省了java层的空间。
方法2:对图片采用软引用,及时地进行recycle()操作
虽然,系统能够确认Bitmap分配的内存最终会被销毁,但是由于它占用的内存过多,所以很可能会超过java堆的限制。因此,在用完Bitmap时,要及时的recycle掉。recycle并不能确定立即就会将Bitmap释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。
SoftReference&Bitmap&
bitmap = new SoftReference&Bitmap&(pBitmap);
if(bitmap != null){
if(bitmap.get() != null && !bitmap.get().isRecycled()){
bitmap.get().recycle();
方法3:&单个页面,横竖屏切换N次后 OOM
<span style="color:#.&看看页面布局当中有没有大的图片,比如背景图之类的。去除xml中相关设置,改在程序中设置背景图(放在onCreate()方法中):
Drawable bg = getResources().getDrawable(R.drawable.bg);
XXX.setBackgroundDrawable(rlAdDetailone_bg);
在Activity destory时注意,bg.setCallback(null);&防止Activity得不到及时的释放。
<span style="color:#.&跟上面方法相似,直接把xml配置文件加载成view&再放到一个容器里,然后直接调用 this.setContentView(View view);避免xml的重复加载。
方法4:在页面切换时尽可能少地重复使用一些代码。比如:重复调用数据库,反复使用某些对象等等.....
方法5:Android堆内存也可以自己定义大小和优化Dalvik虚拟机的内存
--http://blog.csdn.net/wenhaiyan/article/details/5519567
&&&&注意:若使用这种方法:project build target&只能选择 &= 2.2 版本,否则编译将通不过。所以不建议用这种方式。
五、Android中内存泄露监测
内存监测工具 DDMS --& Heap
使用方法比较简单:
&&&&&&&&&选择DDMS视图,并打开Devices视图和Heap视图
&&&&&&&&&点击选择要监控的进程,比如:上图中我选择的是system_process
&&&&&&&&&选中Devices视图界面上的&update heap&&图标
&&&&&&&&&点击Heap视图中的&Cause GC&&按钮(相当于向虚拟机发送了一次GC请求的操作)
在Heap视图中选择想要监控的Type,一般我们会观察dataobject的&total size的变化,正常情况下total size的值会稳定在一个有限的范围内,也就说程序中的代码良好,没有造成程序中的对象不被回收的情况。如果代码中存在没有释放对象引用的情况,那么data object的total size在每次GC之后都不会有明显的回落,随着操作次数的增加而total size也在不断的增加。(说明:选择好data object后,不断的操作应用,这样才可以看出total size的变化)。如果totalsize确实是在不断增加而没有回落,说明程序中有没有被释放的资源引用。那么我们应该怎么来定位呢?
Android中内存泄露定位
通过DDMS工具可以判断应用程序中是否存在内存泄漏的问题,那又如何定位到具体出现问题的代码片段,最终找到问题所在呢?内存分析工具MAT Memory Analyzer Tool解决了这一难题。MAT工具是一个Eclipse 插件,同时也有单独的RCP 客户端,MAT工具的解析文件是.hprof,这个文件存放了某进程的内存快照。MAT工具定位内存泄漏具体位置的方法如下:
&&&① 生成.hprof文件。Eclipse中生成.hprof文件的方法有很多,不同Android版本中生成.hprof的方式也稍有差别,但它们整体思路是一样的。我们在DDMS界面选中想要分析的应用进程,在Devices视图界面上方的一行图标按钮中,同时选中“Update Heap”和“Dump HPROF file”两个按钮,这时DDMS将会自动生成当前选中进程的.hprof文件。
&&&② 将.hprof 文件导入到MAT工具中,MAT工具会自动解析并生成报告,点击“Dominator Tree”按钮,并按包分组,选择已定义的包类点右键,在弹出的菜单中选择List objects?﹥With incoming references,这时会列出所有可疑的类。右键点击某一项,并选择Path to GC Roots?﹥excludeweak/soft references,MAT工具会进一步筛选出跟程序相关的所有内存泄漏的类。这样就可以追踪到某一个产生内存泄漏的类的具体代码中。
&&&使用MAT内存分析工具查找内存泄漏的根本思路是找到哪个类的对象的引用没有被释放,然后分析没有被释放的原因,最终定位到代码中哪些片段存在着内存泄漏。
参考博文:
http://blog.csdn.net/ronaldong99/article/details/8227818
http://articles.e-/embedded/article103890.htm
http://jackxlee./450
http://smallwoniu./8875&
正要总结的,辛苦了~
本分类共有文章18篇,更多信息详见
& 2012 - 2016 &
&All Rights Reserved. &
/*爱悠闲图+*/
var cpro_id = "u1888441";经检测你所在的网络可能存在爬虫,因资源限制,我们只能拒绝你的请求。
如果你是推酷的用户,可以以继续访问使用。
如有疑问,可将IP信息发送到
请求解封。2012年4月 VC/MFC大版内专家分月排行榜第一
2012年5月 VC/MFC大版内专家分月排行榜第二2012年3月 VC/MFC大版内专家分月排行榜第二2011年7月 VC/MFC大版内专家分月排行榜第二2011年1月 VC/MFC大版内专家分月排行榜第二2010年12月 VC/MFC大版内专家分月排行榜第二2010年9月 VC/MFC大版内专家分月排行榜第二2010年6月 VC/MFC大版内专家分月排行榜第二2010年5月 VC/MFC大版内专家分月排行榜第二2010年4月 VC/MFC大版内专家分月排行榜第二
2009年7月 总版技术专家分月排行榜第二2009年3月 总版技术专家分月排行榜第二2009年1月 总版技术专家分月排行榜第二2005年7月 总版技术专家分月排行榜第二2005年5月 总版技术专家分月排行榜第二2005年3月 总版技术专家分月排行榜第二
优秀小版主2015年8月优秀小版主2015年9月优秀小版主2015年5月优秀小版主2015年2月论坛优秀版主
本帖子已过去太久远了,不再提供回复功能。

我要回帖

更多关于 内存泄露解决方法 的文章

 

随机推荐