关闭

内存溢出问题

标签: android内存溢出
89人阅读 评论(0) 收藏 举报

如何定位和解决Andorid的内存溢出问题(大总结)

分类: android  |  标签: 内存溢出,虚拟机,内存泄漏,oom,bitmap  |  作者: zhoubin1992 相关  |  发布日期 : 2015-06-28  |  热度 : 8°

我们经常在做项目过程中遇到内存溢出的问题,同时面试中关于OOM的问题也常常出现。


这里,我将前辈们解决Andorid内存溢出的方法重新整理一番,方便自己以后使用。最后附上参考博文。


一、Android的内存机制

android应用层是由java开发的,android的davlik虚拟机与jvm也类似,只不过它是基于寄存器的。在java中,通过new为对象分配内存,所有对象在java堆内分配空间;而内存的释放是由垃圾收集器(GC)来回收的。 Java采用了有向图的原理。Java将引用关系考虑为图的有向边,有向边从引用者指向引用对象。线程对象可以作为有向图的起始顶点,该图就是从起始顶点(GC roots)开始的一棵树,根顶点可以到达的对象都是有效对象,GC不会回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么我们认为这个(这些)对象不再被引用,可以被GC回收。


二、Android的内存溢出原因

1、内存泄露导致

由于我们程序的失误,长期保持某些资源(如Context)的引用,垃圾回收器就无法回收它,当然该对象占用的内存就无法被使用,这就造成内存泄露。

Android 中常见就是Activity被引用在调用finish之后却没有释放,第二次打开activity又重新创建,这样的内存泄露不断的发生,则会导致内存的溢出。

Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。Android为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程.
2、占用内存较多的对象

保存了多个耗用内存过大的对象(如Bitmap)或加载单个超大的图片,造成内存超出限制。

 

三、常见的内存泄漏问题及其解决方案


1、引用没释放造成的内存泄露

1.1注册没取消造成的内存泄露

 这种Android的内存泄露比纯Java的内存泄漏还要严重,因为其他一些Android程序可能引用系统的Android程序的对象(比如注册机制)。即使Android程序已经结束了,但是别的应用程序仍然还有对Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。

1.2集合中对象没清理造成的内存泄露

我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了

1.3 static

 static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。

    private static ActivitymContext;       //省略

如何才能有效的避免这种引用的发生呢?

    第一,应该尽量避免static成员变量引用资源耗费过多的实例,比如Context。

    第二、Context尽量使用ApplicationContext,因为Application的Context的生命周期比较长,引用它不会出现内存泄露的问题。

  • 看使用的周期是否在activity周期内,如果超出,必须用application;常见的情景包括:AsyncTask,Thread,第三方库初始化等等。
  • 还有些情景,只能用activity:比如,对话框,各种View,需要startActivity的等。
  • 总之,尽可能使用Application。

    第三、使用WeakReference代替强引用。比如可以使用WeakReference<Context>mContextRef;

1.4、线程(内部类的使用)

线程产生内存泄露的主要原因在于线程生命周期的不可控。如果我们的线程是Activity的内部类,所以MyThread中保存了Activity的一个引用,当MyThread的run函数没有结束时,MyThread是不会被销毁的,因此它所引用的老的Activity也不会被销毁,因此就出现了内存泄露的问题。

如果非静态内部类的方法中,有生命周期大于其所在类的,那就有问题了。比如:AsyncTaskHandler,这两个类都是方便开发者执行异步任务的,但是,这两个都跳出了Activity/Fragment的生命周期。

解决方案

    第一、将线程的内部类,改为静态内部类。

       原因:

因为非静态内部类会自动持有一个所属类的实例,如果所属类的实例已经结束生命周期,但内部类的方法仍在执行,就会hold其主体(引用)。也就使主体不能被释放,亦即内存泄露。静态类编译后和非内部类是一样的,有自己独立的类名。不会悄悄引用所属类的实例,所以就不容易泄露。

    第二、如果需要引用Acitivity,使用弱引用。

 

2、资源对象没关闭造成的内存泄露

资源性对象比如(CursorFile文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。而不是等待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:等比例缩小图片

有时候,我们要显示的区域很小,没有必要将整个图片都加载出来,而只需要记载一个缩小过的图片,这时候可以设置一定的采样率,那么就可以大大减小占用的内存。

1 BitmapFactory.Options options = new BitmapFactory.Options();
2 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释放掉,但是会给虚拟机一个暗示:“该图片可以释放了”。


1 SoftReference<Bitmap> bitmap;
2                bitmap = new SoftReference<Bitmap>(pBitmap);
3     
4   if(bitmap != null){ 
5