Android常见的内存泄露场景

心情么么哒,又可以白嫖咯...

常见概念

  1. 什么是内存泄露?

    该被GC回收的内存没有被回收。讲人话:短生命周期的引用被长生命周期的对象持有,导致短生命周期的对象的内存无法被GC回收。举个例子:非静态内部类默认持有外部类的引用,不幸的是,我们在外部类创建了一个该内部类的静态实例。静态实例的生命周期与长与普通实例的生命周期,而静态实例是可以作为GCRoots的根节点的,导致GC在进行可达性分析时,外部类的引用一直在引用链上,不被回收。这样就导致了外部类对象的内存泄露。

    持续的内存泄露会导致内存溢出

  2. 什么是内存溢出?

    ** 程序申请内存时,内存空间没有足够的内存分配,就会导致内存溢出**。类比生活中的例子:假设你朋友租的房子不大,且他不注意卫生,每次消费完(喝小酒,撸个串...)都不打扫卫生,一天两天还好,时间长了...呵呵,反正房子就那么大,垃圾多了,躺个地都没了。

    Android中出现内存溢出,是一件大事,因为你的应用会被强制退出。

  3. 什么是内存抖动?

    短时间内有大量的对象被创建和回收。类比生活中的正弦函数,在半个周期内,我们不断的创建对象,申请内存,很快就攀到了波峰,接着出现了GC(Stop The World),清理内存,几乎一瞬间就到了起步线。这样一个急上急下的折线图,就是内存抖动的表现。

    Android的UI卡顿就是内存抖动导致,移步了解UI卡顿与内存抖动的前世今生

Android 的虚拟机是基于寄存器的Dalvik/ART,它的最大堆大小一般是16M,但是根据不同的ROM,这个最大值有可能不一样。如果我们占用的内存超出ROM给出的最大值,那么一定会出现OOM。

Java内存分配策略

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

  • 静态存储区(方法区):存放静态数据,全局static数据和常量,生命周期最长,即程序啥时候退出我啥时候让出来内存。
  • 栈区:当方法被执行时,方法体内的局部变量(其中包括基础数据类型,对象引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放,因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆区:通常就是指在程序运行时直接new出来的内存,也就是对象的实例,这部分内存在不使用时将由GC来负责回收。

栈与堆的区别

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

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

    public class Sample {
        int s1 = 0;
        Sample mSample1 = new Sample();
        public void method() {
            int s2 = 1;
            Sample mSample2 = new Sample();
        }
    }
    Sample mSample3 = new Sample();

Sample类的局部变量s2和引用变量mSample2都是存放在栈中的,但mSample2指向的对象是存放在堆上的,mSample3指向的对象实体也存放在堆上,包括这个对象的所有成员变量s1和mSample1。注意:当方法退出后,mSample2引用在栈中的内存会被回收,它指向的堆内存上的对象需要等GC来回收。

总结:

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

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

Java是如何管理内存的

Java内存管理解决了对象的分配和释放问题。在Java中,我们只需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都在堆(heap)中分配空间,另外对象的释放是由GC决定和执行的。这种分工,方便了我们,却加重了JVM的工作,因为清理垃圾的工作是很消耗CPU的。

Java是如何精确判断哪些内存需要释放的?大家请移步至该文章可达性分析与计数器

Android中常见的内存泄漏

  1. 集合类泄漏

    如果我们仅仅调用集合类的添加方法,而没有执行相应的删除,且这个集合类的引用是个类变量(比如说类中的静态属性,全局性的map等,即有静态引用或final一直指向它),那么集合中的元素的对象占用的内存就会泄露。

  2. 单例造成的内存泄漏

    由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当,很容易造成内存泄漏,比如说下面一个典型的例子

        public class AppManager {
            private static AppManager instance;
            private Context context;
            private AppManager(Context context) {
                this.context = context;
            }
            public static AppManager getInstance(Context context) {
                if (instance == null) {
                    instance = new AppManager(context);
                }
                return instance;
            }
        }
        
    
    1. 如果此时传入的是Application的Context,因为Application的生命周期就是整个应用的生命周期,所以这将没没有任何问题。

    2. 如果此时传入的是Activity的Context,当这个Context所对应的Activity退出时,由于该Context的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当Activity退出时它的内存并不会被回收,这就造成了内存泄漏。

  3. 静态全局变量

    public class MyActivity extends Activity {
        
        private static Drawable sBackground;
        
        @override
        protect void onCreate(Bundle state) {
            super.onCreate(state);
            TextView table = new TextView(this);
            table.setText("title");
            if(sBackground == null) {
                sBackground = getDrawable();
            }
            table.setBackgroundDrwable(sBackground);
            setContentView(table);
        }
    }
    
    

    sBackground 是一个静态全局变量,它的生命周期跟随类实例而不是对象实例,因此它会在应用退出后后才消失。而此时,我们将drawable与TextView结合,而TextView含有当前 this 的引用,所以该 activity 的实例依然在内存中,引用链如下:sBackground -> table -> this。
    我们知道GC是不会回收存在引用链的对象的,所以 activity实例占用的内存不会被回收。

  4. 内部类(非静态内部类和匿名内部类)

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

         public class MainActivity extends AppCompatActivity {
            private static TestResource mResource = null;
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
                if(mManager == null){
                    mManager = new TestResource();
                }
                //...
            }
            class TestResource {
            //...
            }
         }
    

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

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

    image

    其中 NO1表示Application和Service可以启动一个Activity,不过需要创建一个新的task任务队列,而对于Dialog而言只有在Activity中才能创建。

    匿名内部类

    Android开发过程中,如果在Activity/Fragment/View中,使用了匿名内部类,并被异步线程持有了,那就要小心了,如果没有任何防护措施,一定会导致泄漏。

    public class MainActivity extends Activity {
        ...
        Runnable ref1 = new MyRunable();
        Runnable ref2 = new Runnable() {
            @Override
            public void run() {
    
            }
        };
        ...
    }
    

    ref1和ref2的区别是,ref2使用了匿名内部类,我们来看看运行时这两个引用的内存


    image

    可以看到,ref2这个匿名类的实现对象里面多了一个引用,这个引用指向MainActivity.this,也就是说当前的MainActivity实例会被ref2持有,如果这个引用再传入一个异步线程,且线程的生命周期此Activity生命周期长的时候(发出了一个复杂的I/O请求),就会造成内存泄漏。如何解决这类问题那?
    新建一个静态内部类。但是有一个问题还需要注意:如果在 run()中,使用了activity里面的资源,一定要判空哦。

  5. Handler造成的内存泄漏

    Handler的使用造成的内存泄漏问题应该说是最为常见的,很多时候我们为了避免ANR而不再主线程中进行耗时操作,在处理网络任务或者封装一些请求回调等API都借助Handler来处理。
    注意:Handler不是万能的,对于Handler的使用,一不小心就有可能造成内存泄漏。为什么那?
    举个例子:请看下面的代码

    public class SampleActivity extends Activity {
        private final Handler mLeakyHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                // ...
            }
        }
        private final Handler secondHandler = new Handler();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            // Post a message and delay its execution for 10 minutes.
            mLeakyHandler.sendMessageAtTime(2*1000*60)
            secondHandler.postRunnable({
            }, 1000*60*2)
    
            // Go back to the previous Activity.
            finish();
        }
    }
    

    首先我们知道:Android中的 main线程只有在应用退出的时候才退出。现在我们在Activity中声明一个handler,使用它发送一个一个延迟2分钟执行的消息。当该Activity被finish掉时,我们也没有将该消息从MessgaeQueue中移除,那么这个消息还会继续存在于主线程的MessageQueue中。第一个handler我们使用了匿名内部类的方式创建了handler,导致它持有了该Activity实例的引用,所以此时finish掉的Activity就不会被回收,从而导致造成activity的内存泄漏;第二个handler虽然没有使用匿名内部类方式创建,但是它发送了一个runnable消息,而runnable是匿名内部类,所以,它也会造成内存泄露。
    解决这类内存泄露的方法:在onStop的时候从MessageQueue中移出去吧。

  6. Bitmap导致的内存泄露

    在Java中对象的引用类型分为四种:


    image

    在Android应用的开发中,为了防止内存溢出,在处理一些占用大量而且生命周期比较长的对象时候,可以进来应用软引用和弱引用技术。

    软引用、弱引用和一个引用队列联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中,利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

    假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方都会用到,如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低,所以我们考虑把图片缓存起来,需要的时候直接从内存中读取,但是由于图片占用内存空间较大,缓存很多图片需要很多内存,就可能比较容易发生OOM异常,这个时候我们就可以考虑使用软/弱引用来避免这个问题。

    首先定义一个HashMap,保存软引用对象

    private Map <String, SoftReference<Bitmap>> imageCache =
        new HashMap <String, SoftReference<Bitmap>> ();
    

    使用软引用之后,在OOM异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。

    如果只是想避免OOM异常的发生,则可以使用软引用,如果对于性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用

    另外可以根据对象是否经常使用来判断选择软引用还是弱引用,如果该对象经常使用就尽量使用软引用,如果该对象不被使用的可能性更大些就可以使用弱引用

  7. 避免override finalize()

    • finalize()方法被执行的时间不确定,不能依赖于它来释放紧缺的资源,时间不确定的原因是:虚拟机调用GC的时间不确定 Finalize daemon线程被调用的时间不确定。

    • finalize方法只会被执行以次,即使对象被复活了,如果已经执行过finalize方法再次被GC时也不会再执行了,原因是:含有finalize方法的object是在new 的时候由虚拟机生成一个finalize Reference在来引用到该Object的,而在finalize方法执行的时候,该Object所对应的finalize Reference会被释放掉,即使在这个时候把object复活,再第二次被GC的时候由于没有了finalize Reference与之对应,所以finalize不会再执行。

    • 含有Finalize方法的object需要至少经历两次GC才有可能被释放。

  8. 资源未关闭而造成的内存泄漏

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

总结

对Activity等组件的引用应该控制在Activity的生命周期内,如果不能就考虑使用getApplicationContext或者getApplication,避免Activity被外部长生命周期的对象引用而泄漏

尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context),如果不能避免使用,要考虑及时把外部成员变量置空,也可以在内部类中使用弱引用来引用外部类的变量

对于长生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样避免内存泄漏

将内部类改为静态内部类

静态内部类中使用弱引用来引用外部类的成员变量

Handler的持有的引用对象最好使用弱引用,资源释放也可以情况Handler里面的消息,比如在Activity onStop或者onDestroy的时候取消哎Handler对象的Message和Runnable

在Java的实现过程中,也要考虑其对象释放,最好的办法是在不使用某个对象时,显式的将此对象赋值为null,比如使用完Bitmap后先调用recycle,在赋值为null,清空对图片等资源又直接引用或间接引用的数组,最后遵循谁先创建释放谁的原则

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

好了,到此就内存泄露以及Android中常见的内存泄露场景就介绍的差不多。当然,以上观点仅仅是小编的管中窥豹,肯定还有许多未知的场景没有覆盖。如果有疑问或不对的地方,请不吝赐教。如果觉得有收获,请点赞,喜欢哦~

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

推荐阅读更多精彩内容