Android之内存溢出和内存泄漏的原因和解决方案

基础

JAVA是在JVM所虚拟出的内存环境中运行的,内存分为三个区:堆、栈和方法区。

  • 栈(stack):是简单的数据结构,程序运行时系统自动分配,使用完毕后自动释放。优点:速度快。
  • 堆(heap):用于存放由new创建的对象和数组。在堆中分配的内存,一方面由java虚拟机自动垃圾回收器来管理,另一方面还需要程序员提供修养,防止内存泄露问题。
  • 方法区(method):又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。

概念

  • 内存溢出(Out of Memory):系统会给每个APP分配内存也就是Heap Size值。当APP占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存时就会抛出的Out Of Memory异常。

  • 内存泄漏(Memory Leak):当一个对象不在使用了,本应该被垃圾回收器(JVM)回收。但是这个对象由于被其他正在使用的对象所持有,造成无法被回收的结果。内存泄漏最终会导致内存溢出。

  • 内存抖动:内存抖动是指在短时间内有大量的对象被创建或者被回收的现象,主要是循环中大量创建、回收对象。这种情况应当尽量避免。
    它们三者的重要等级分别:内存溢出 > 内存泄露 > 内存抖动。
    内存溢出对我们的App来说,影响是非常大的。有可能导致程序闪退,无响应等现象,因此,我们一定要优先解决OOM的问题。

  • 强引用:强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。 当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

  • 软引用:如果一个对象只具有软引用,但内存空间足够时,垃圾回收器就不会回收它;直到虚拟机报告内存不够时才会回收, 只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

  • 弱引用:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

  • 虚引用:虚引用可以理解为虚设的引用,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。
    虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。 如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

关系

  • 内存泄漏是造成应用程序OOM的主要原因之一。由于Android系统为每个应用程序分配的内存有限,当一个应用中产生的内存泄漏比较多时,就难免会导致应用所需要的内存超过这个系统分配的内存限额,这就造成了内存溢出而导致应用Crash。

  • 我们的App多次出现内存泄露,可能就会导致内存溢出。但是,我们的App出现内存溢出,不一定就是因为内存泄露,因为本身Android系统分配给每一个的App的空间就是那么一点。另外,内存泄露也不一定就会出现内存溢出,因为还是泄露的速度比较慢,系统将进程杀死了,也就不会内存溢出。不过,发现内存泄露,我们还是要第一时间解决。

危害

  • 内存溢出:会触发Java.lang.OutOfMemoryError,造成程序崩溃。
  • 内存泄漏:过多的内存泄漏会造成内存溢出,同样也会造成相关UI的卡顿现象。

判断是否有内存泄露的工具

LeackCanary

Memory Monitor

DDMS

处理方式汇总

强引用,软引用和弱引用

  • 释放强引用,使用软引用和弱引用;

大量的图片、音频、视频处理,当在内存比较低的系统上也容易造成内存溢出

  • 建议使用第三方,或者JNI来进行处理;

Bitmap对象的处理

  • 不要在主线程中处理图片

  • 使用Bitmap对象要用recycle释放

     // Bitmap对象没有被回收
     if (!bitmapObject.isRecyled()) {
         // 释放  
         bitmapObject.recycle(); 
         // 提醒系统及时回收 
         System.gc(); 
         }  
    
  • 控制图片的大小,压缩大图,高效处理,加载合适属性的图片。
    当我们有些场景是可以显示缩略图的时候,就不要调用网络请求加载大图,例如在RecyclerView中,我们在上下滑动的时候,就不要去调用网络请求,当监听到滑动结束的时候,才去加载大图,以免上下滑动的时候产生卡顿现象。

非静态内部类和匿名內部类Handler、Thread、Runnable等由于持有外部类Activity的引用,从而关闭activity,线程未完成造成内存泄漏

  • 在Activity中创建非静态内部类,非静态内部类会持有Activity的隐式引用,若内部类生命周期长于Activity,会导致Activity实例无法被回收。(屏幕旋转后会重新创建Activity实例,如果内部类持有引用,将会导致旋转前的实例无法被回收)。

  • 如果一定要使用内部类,就改用static内部类,在内部类中通过WeakReference的方式引用外界资源。对Handler、Thread、Runnable等使用弱引用,并且调用removeCallbacksAndMessages等移除。
    举例:在下面这段代码中存在一个非静态的匿名类对象Thread,会隐式持有一个外部类的引用MainActivity 。同理,若是这个Thread作为MainActivity的内部类而不是匿名内部类,他同样会持有外部类的引用。

     public class MainActivity extends AppCompatActivity {
         @Override
         protected void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.main);
             leakFun();
         }
     
         private void leakFun() {
             new Thread(new Runnable() {
                 @Override
                 public void run() {
                     try {
                         Thread.sleep(10 * 1000);
                     } catch (InterruptedException e) {
                         e.printStackTrace();
                     }
                 }
             });
         }
     }
    

在线程休眠的这10s内,会一直隐式持有外部类的引用MainActivity,如果在10s之前,销毁MainActivity,就会报内存泄漏。同理,若是这个Thread作为MainActivity的内部类而不是匿名内部类,也会内存泄漏。
总而言之:如果Activity在销毁之前,任务还未完成, 那么将导致Activity的内存资源无法回收,造成内存泄漏。
解决办法:在这里只需要将为Thread匿名类定义成静态的内部类即可(静态的内部类不会持有外部类的一个隐式引用)。或保证在Activity在销毁之前,完成任务!

  • 在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。

资源未及时关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,Cursor,File,Stream,ContentProvider,Bitmap,动画,I/O,数据库,网络的连接等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

  • 广播BraodcastReceiver:记得注销注册unregisterReceiver();

  • 文件流File:记得关闭流InputStream / OutputStream.close();

  • 数据库游标Cursor:使用后关闭游标cursor.close();

  • 对于图片资源Bitmap:当它不再被使用时,应调用recycle()回收此对象的像素所占用的内存,再赋为null

  • 动画:属性动画或循环动画,在Activity退出时需要停止动画。在属性动画中有一类无限循环动画,如果在Activity中播放这类动画并且在onDestroy中没有去停止动画,那么这个动画将会一直播放下去,这时候Activity会被View所持有,从而导致Activity无法被释放。在Activity中onDestroy去调用objectAnimator.cancel()来停止动画。

     public class LeakActivity extends AppCompatActivity {
         private TextView textView;
     
         @Override
         protected void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_leak);
             textView = (TextView) findViewById(R.id.text_view);
             ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(textView, "rotation", 0, 360);
             objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
             objectAnimator.start();
         }
     }
    
  • 集合对象及时清理,使得JVM回收:我们通常会把对象存入集合中,当不使用时,清空集合,让相关对象不再被引用;

     objectList.clear();
     objectList=null;
    

注销监听器

  • 在onPause()/onDestroy()方法中解除监听器,包括在Android自己的Listener,Location Service或Display Manager Service以及自己写的Listener。

static关键字修饰的变量由于生命周期过长,容易造成内存泄漏

  • static对象的生命周期过长,应该谨慎使用。一定要使用要及时进行null处理。

  • 静态变量Activity和View会导致内存泄漏。例如:context,textView实例的生命周期与应用的生命周期一样,而他们都持有当前Activity的(MainActivity )引用,一旦MainActivity 销毁,而他的引用一直被持有,就不会被回收。所以,内存泄漏就产出了。

     public class MainActivity extends AppCompatActivity{   
     private static Context context;    
     private static TextView textView;  
    
     @Override    
     protected void onCreate(Bundle savedInstanceState){   
         super.onCreate(savedInstanceState);    
         setContentView(R.layout.activity_main);    
         context = this;    
         textView = new TextView(this);
         }    
     }
    

如果使用Context ,尽可能使用Applicaiton的Context

  • 单例模式造成的内存泄漏,如context的使用,单例中传入的是activity的context,在关闭activity时,activity的内存无法被回收,因为单例持有activity的引用。

  • 在context的使用上,应该传入application的context到单例模式中,这样就保证了单例的生命周期跟application的生命周期一样。

  • 因为单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。

  • 单例模式应该尽量少持有生命周期不同的外部对象,一旦持有该对象的时候,必须在该对象的生命周期结束前null

     public class TestManager {
         private static TestManager instance;
         private Context context;
     
         private TestManager(Context context) {
             this.context = context;
         }
     
         public static TestManager getInstance(Context context) {
             if (instance != null) {
                 instance = new TestManager(context);
             }
             return instance;
         }
     }
    

这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
1、传入的是Application的Context:这将没有任何问题,因为单例的生命周期和Application的一样长 ;
2、传入的是Activity的Context:当这个Context所对应的Activity退出时,由于该Context和Activity的生命周期一样长(Activity间接继承于Context),所以当前Activity退出时它的内存并不会被回收,因为单例对象持有该Activity的引用。
所以正确的单例应该修改为下面这种方式:

    public class TestManager {
        private static TestManager instance;
        private Context context;
    
        private TestManager(Context context) {
            this.context = context.getApplicationContext();
        }
    
        public static TestManager getInstance(Context context) {
            if (instance != null) {
                instance = new TestManager(context);
            }
            return instance;
        }
    }

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

不要使用String进行字符串拼接

  • 严格的讲,String拼接只能归结到内存抖动中,因为产生的String副本能够被GC,不会造成内存泄露。
  • 频繁的字符串拼接,使用StringBuffer或者StringBuilder代替String,可以在一定程度上避免OOM和内存抖动。

三方库

  • 对于EventBus,RxJava等一些第三方开源框架的使用,若是在Activity销毁之前没有进行解除订阅将会导致内存泄漏。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容

  • 前言 之前研究过一段时间关于 Android 内存泄漏的知识,大致了解了导致内存泄漏的一些原因,但是没有深入去探究...
    Zackratos阅读 19,150评论 19 49
  • Android 内存管理的目的 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。简单粗...
    晨光光阅读 1,285评论 1 4
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 1,619评论 0 8
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,359评论 0 12
  • 今天是E战到底特训营第四期最后一次打卡了,How times fly! 21天转瞬即逝,回顾这段时间,一个字“充实...
    重阳2308阅读 190评论 0 0