内存泄漏

内存泄漏

概念:
指程序在申请内存后,当该内存不需再使用 但 却无法被释放 & 归还给 程序的现象。即 程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。

表现:
它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。

原因:
造成内存泄漏的本质原因是 持有引用者的生命周期 大于 被引用者的生命周期

影响:
内存泄漏一般不会导致程序异常,但它会导致程序的内存占用过大,这将提高内存溢出的几率。所以,内存泄露是内存溢出的一种原因之一,但不是唯一因素。

导致内存泄漏的原因及解决方案:

1、集合类导致的内存泄漏

原因:
集合类 添加元素后,仍引用着 集合元素对象,导致该集合元素对象不可被回收,从而 导致内存泄漏

代码演示:

    private void memoryLeakOfCollection() {
        // 通过 循环申请Object 对象 & 将申请的对象逐个放入到集合List
        List<Object> objectList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
        // 虽释放了集合元素引用的本身:o=null)
        // 但集合List 仍然引用该对象,故垃圾回收器GC 依然不可回收该对象
    }

解决方案:
集合类 添加集合元素对象 后,在使用后必须从集合中删除

        objectList.clear();
        objectList=null;

2、静态变量导致的内存泄漏

原因:
静态变量是在类被load的时候分配内存的,并且存在于方法区。当类被卸载的时候,静态变量被销毁。只要静态变量没有被销毁也没有置null,其对象一直被保持引用,也即引用计数不可能是0,因此不会被垃圾回收。
所以 Static 关键字修饰的成员变量的生命周期 = 应用程序的生命周期。

代码演示:

    public class YouhuaTestActivity extends AppCompatActivity {
        private static Context mContext;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_youhua_test);
            // Activity无法正常销毁,因为静态变量mContext引用了它。
            mContext = this;
        }
    }

解决方案:
A、尽量避免 Static 成员变量引用资源耗费过多的实例(如 Context),若需引用 Context,则尽量使用Applicaiton的Context
B、使用 弱引用(WeakReference) 代替 强引用 持有实例

静态变量引起内存泄漏的一个特殊例子--单例模式:

原因:
当单列中传入一个Activity的Context后,该单列就持有Activity的引用,我们知道单例的生命周期和Application的一样长,所以当Activity退出时它的内存并不会被回收。

代码演示:

    private AppManager(Context context) {
        // 创建单例时,需传入一个Context
        // 若传入的是Activity的Context,此时单例 则持有该Activity的引用
        // 由于单例一直持有该Activity的引用(直到整个应用生命周期结束),即使该Activity退出,该Activity的内存也不会被回收
        // 特别是一些庞大的Activity,此处非常容易导致OOM
        this.context = context;
    }

解决方案:
论传入什么类型的Context,最终单例使用的都是Application的Context

    private AppManager(Context context) {
        this.context = context.getApplicationContext();
    }

3、非静态内部/匿名类
非静态内部类默认持有外部类的引用;而静态内部类则不会。

3.1、非静态内部类的实例是静态时 引起当内存泄漏

原因:
当非静态内部类当实例是静态时,那么这个实例当生命周期就和程序当生命周期一样长,而非静态内部类持有外部类都引用,导致外部类无法被释放,最终造成内存泄漏。

代码演示:

    public class TestActivity extends AppCompatActivity {
        // 非静态内部类的实例的引用是静态当 
        public static InnerClass innerClass = null;
        
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            // 保证非静态内部类的实例只有1个
            if (innerClass == null)
                innerClass = new InnerClass();
        }
        // 定义非静态内部类
        private class InnerClass {
            
        }
    }

解决方案:
1、尽量避免非静态内部类所创建的实例 = 静态;将非静态内部类设置为静态内部类(因为静态内部类默认不持有外部累当引用)。
2、将内部类抽取出来封装成一个单例

3.2、多线程:AsyncTask、实现Runnable接口、继承Thread类 引起的内存泄漏

泄漏原因:
当工作线程正在处理任务且外部类需销毁时,由于工作线程实例持有外部类引用,将使得外部类无法被垃圾回收器(GC)回收,从而造成 内存泄露。

代码:

public class TestActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 异步任务和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用。
        // 如果Activity在销毁之前,任务还未完成,那么将导致Activity的内存资源无法回收,造成内存泄漏
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                SystemClock.sleep(10000);
                return null;
            }
        }.execute();

        new Thread(new Runnable() {
            @Override
            public void run() {
                SystemClock.sleep(10000);
            }
        }).start();

        // 通过创建的内部类 实现多线程
        new MyThread().start();
    }

    // 自定义的Thread子类
    private class MyThread extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

优化方案一:使用静态内部类

public class TestActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        new Thread(new MyRunnable()).start();
        new MyAsyncTask(this).execute();
    }

    private static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        private WeakReference<Context> weakReference;
        public MyAsyncTask(Context context) {
            weakReference = new WeakReference<>(context);
        }
        @Override
        protected Void doInBackground(Void... params) {
            SystemClock.sleep(10000);
            return null;
        }
        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            MainActivity activity = (MainActivity) weakReference.get();
            if (activity != null) {
                //...
            }
        }
    }
    
    private static class MyRunnable implements Runnable{
        @Override
        public void run() {
            SystemClock.sleep(10000);
        }
    }
}

优化方案二:强制结束线程
当 外部类(此处以Activity为例) 结束生命周期时(此时系统会调用onDestroy()),强制结束线程(调用stop()),使得工作线程实例的生命周期与外部类的生命周期 同步

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Thread.stop();
        // 外部类Activity生命周期结束时,强制结束线程
    }

3.3、Handler造成的内存泄漏

Handler内存泄漏分析

原因:
大家平时开发中喜欢在Activity中直接new一个Handler的匿名内部类,在Java中,非静态的内部类或者匿名类会隐式的持有其外部类的引用,而静态的内部类则不会。这样造成匿名内部类持有一个外部类(通常是Activity)的引用(不然怎么更新ui),但是Handler常常伴随着一个执行耗时操作的异步线程(如下载多张图片),如果在完成耗时操作之前,Activity退出,异步线程持有handler的引用,handler持有Activity的引用,从而导致内存泄漏。

代码:

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // do something
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }
    private void loadData(){
        //...do request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

优化方案:

public class MainActivity extends AppCompatActivity {
    /**
     * 创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,
     * 这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,
     * 所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息,
     */
    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
                activity.mTextView.setText("");
            }
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView)findViewById(R.id.textview);
        loadData();
    }
    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 移除消息队列中所有消息和所有的Runnable,
        mHandler.removeCallbacksAndMessages(null);
        // 当然,也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();来移除指定的Runnable和Message。
    }
}

4、资源对象未关闭造成当内存泄漏

原因:
对于资源的使用(如 广播BraodcastReceiver、文件流File、数据库游标Cursor、图片资源Bitmap等),
若在Activity销毁时无及时关闭 / 注销这些资源,这些资源将不会被回收,从而造成内存泄漏。

优化方案:
在Activity销毁时 及时关闭 / 注销资源

如下:
4.1、BraodcastReceiver 及时注销:unregisterReceiver()
4.2、文件流File:及时关闭流:InputStream / OutputStream.close()
4.3、数据库游标cursor使用后关闭游标:cursor.close()
4.4、图片资源Bitmap:Android分配给图片的内存只有8M,若1个Bitmap对象占内存较多,当它不再被使用时,应调用recycle()回收此对象的像素所占用的内存;最后再赋为null
4.5、自定义View时TypedArray使用完后忘记调用recycle()方法释放内存

Bitmap.recycle();
Bitmap = null;

4.5、属性动画中有一类无限循环的动画,如果在Activity中播放此动画且没有在onDestory中停止动画,即使已经无法在界面上看到动画效果,动画也会一直播放下去,并且这个时候Activity的View会被动画持有,而View有持有了Activity,最终导致Activity无法释放。这种泄漏的解决办法是在onDestory中调用animator.cancal()来停止动画。

5、上下文对象导致的内存泄漏

5.1、使用application的context来替代activity相关的context。
尽量避免activity的context在自己的范围外被使用,这样会导致activity无法释放。不要让生命周期长于Activity的对象持有到Activity的引用
5.2、在Android中,Application Context的生命周期和应用的生命周期一样长,而不是取决于某个Activity的生命周期。
如果想保持一个长期生命的对象,并且这个对象需要一个 Context,就可以使用Application对象。可以通过调用Context.getApplicationContext()方法或者 Activity.getApplication()方法来获得Application对象。
5.3、Drawable的对象的内部Callback持有activity的引用,当Activity finish()之后,静态成员drawable始终持有这个Activity的引用,导致内存释放不了。
5.4、Activity内部如果有一个Context的成员变量,将导致Context引用指向的Activity对象释放不了,见上文:静态变量导致的内存泄漏

6、webview导致当内存泄漏

http://lipeng1667.github.io/2016/08/06/memory-optimisation-for-webview-in-android/

7、其他

未采用软引用等

  • 软引用:
    如果一个对象只具有软引用,那么如果内存空间足够,垃圾回收器就不会回收它;如果内存 空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队 列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
  • 弱引用:
    如果一个对象只具有弱引用,那么在垃圾回收器线程扫描的过程中,一旦发现了只具有弱引 用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱 引用也可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联 的引用队列中。
  • 弱引用与软引用的根本区别在于:
    只具有弱引用的对象拥有更短暂的生命周期,可能随时被回收。而只具有软引用的对象只有当内存不够的时候才被回收,在内存足够的时候,通常不被回收。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 1,611评论 0 8
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,357评论 0 12
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    神奇的小蘑菇阅读 520评论 0 0
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    apkcore阅读 1,214评论 2 7
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    DreamFish阅读 789评论 0 5