简单分析Android的垃圾回收与内存泄露

Android系统是运行在Java虚拟机上的,作为嵌入式设备,内存往往非常有限,了解Android的垃圾回收机制,可以有效的防止内存泄露问题或者OOM问题。本文作为入门文章,将浅显的讨论垃圾回收与内存泄露的原理,不讨论Dalvik虚拟机底层机制或者native层面的问题。

1. 基础

在分析垃圾回收前,我们要复习Java与离散数学的基础。

  • 实例化:对象是类的一个实例,创建对象的过程也叫类的实例化。对象是以类为模板来创建的。比如Car car = new Car();,我们就创造了一个Car的实例(Create new class instance of Car)

  • 引用:某些对象的实例化需要其它的对象实例,比如ImageView的实例化就需要Context对象,就是表示ImageView对于Context持有引用(ImageView holds a reference to Context)。

  • 有向图:在每条边上都标有有向线段的图称为有向图,Java中的garbage collection采用有向图的方式进行内存管理,箭头的方向表示引用关系,比如 B ← A ,就是B中需要A,B引用A。

  • 可达:有向图D={S,R}中,对于Si,Sj 属于S,如果从Si到Sj有任何一条通路存在,则可称Si可达Sj。也就是说,当B ← A中间箭头断了,就称作不可达,这时A就不可达B了。

  • Shallow heap 与 Retain heap 的对比

  • Shallow heap表示当前对象所消耗的内存

  • Retained heap表示当前对象所消耗的内存加上它引用的内存总合


    Google I/O 2011: Memory management for Android Apps


    上图的橙色的Object是该有向图的起点,它的Shallow heap是100,而它的Retained heap是100 + 300 = 400。

  • 2. 什么是垃圾回收

    Java GC(Garbage Collection,垃圾收集,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢。这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制。概括地说,该机制对虚拟机中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息(Nerver Stop)的保证虚拟机中的内存空间,防止出现内存泄露和溢出问题。

    3. 什么情况需要垃圾回收

    对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常GC采用有向图的方式记录并管理堆中的所有对象,通过这种方式确定哪些对象时“可达”,哪些对象时“不可达”。当对象不可达的时候,即对象不再被引用的时候,就会被垃圾回收。

    网上有很多文档介绍可达的关系了,如图,在第六行的时候,o2改变了指向,Obj2就不再引用main的了,即他它们是不可达的,Obj2就可能在下次的GC中被回收。


    developerWorks Java technology


    4. 什么是内存泄露

    当你不再需要某个实例后,但是这个对象却仍然被引用,防止被垃圾回收(Prevent from being bargage collected)。这个情况就叫做内存泄露(Memory Leak)。

    下面将以How to Leak a Context: Handlers & Inner Classes这篇文章翻译为例,介绍一个内存泄露。

    看如下的代码

    public class SampleActivity extends Activity {
    
      private final Handler mLeakyHandler = new Handler() {    @Override
        public void handleMessage(Message msg) {      // ... 
        }
      }
    }

    当你打完这个代码后,IDE应该就会提醒你

    In Android, Handler classes should be static or leaks might occur.

    它到底是如何泄露的呢?

  • 当你启动一个application时,它会自动在主线程创建一个Looper对象,用于处理Handler中的message。Looper实现了简单的消息队列,在循环中一个接一个的处理Message对象。大多数Application框架事件(比如Activity生命周期调用,按钮点击等)都在Message中,它们在Looper的消息队列中一个接一个的处理。注意Looper是存在于application整个生命周期中。

  • 当你新建了一个handler对象后,它会被分配给Looper的消息队列。被发送到消息队列的Message将保持对Handler的引用,因为当消息队列处理到这个消息时,需要使用[Handler#handleMessage(Message)](http://developer.android.com/reference/android/os/Handler.html#handleMessage(android.os.Message)这个方法。(也就是说,只要没有处理到这个Message,Handler就一直在队列中被引用)

  • 在java中,非静态的内部Class与匿名Class对它们外部的Class有强引用。static inner class除外。


  • 引用关系


    现在,我们尝试运行如下代码

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

    这个程序很简单,我们可以脑补一下,它应该是启动了又瞬间关闭,但是事实真的是关闭了吗?

    稍有常识的人可以看出,它发送了一个Message,将在十分钟后运行,也就是说Message将被保持引用达到10分钟,这就照成了至少10分钟的内存泄露。

    最后正确的代码如下


    ublic class SampleActivity extends Activity {
    
      /**
       * Instances of static inner classes do not hold an implicit
       * reference to their outer class.
       */
      private static class MyHandler extends Handler {
        private final WeakReference<SampleActivity> mActivity;
    
        public MyHandler(SampleActivity activity) {
          mActivity = new WeakReference<SampleActivity>(activity);
        }    @Override
        public void handleMessage(Message msg) {      SampleActivity activity = mActivity.get();      if (activity != null) {        // ...
          }
        }
      }  private final MyHandler mHandler = new MyHandler(this);  /**
       * Instances of anonymous classes do not hold an implicit
       * reference to their outer class when they are "static".
       */
      private static final Runnable sRunnable = new Runnable() {      @Override
          public void run() { /* ... */ }
      };  @Override
      protected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    // Post a message and delay its execution for 10 minutes.
        mHandler.postDelayed(sRunnable, 1000 * 60 * 10);    // Go back to the previous Activity.
        finish();
      }
    }


    结论

  • GC是按照有向图是否可达来判断对象实例是否有用

  • 如果不在需要某个实例,却仍然被引用,这个情况叫做内存泄露

  • 匿名类/非静态类内部class会保持对它所在Activity的引用,使用时要注意它们的生命周期不能超过Activity,否则要用static inner class

  • 善于在Activy中的生命周期(比如onPause)中手动控制其他类的生命周期

  • 以下是AndroidOOM原因总结

    一、什么是OOM
    OOM(out of memory)即内存泄露。一个程序中,已经不需要使用某个对象,但是因为仍然有引用指向它垃圾回收器就无法回收它,当该对象占用的内存无法被回收时,就容易造成内存泄露。
    Android的一个应用程序的内存泄露对别的应用程序影响不大,因为为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,也就是说每个应用程序都是在属于自己的进程中运行的。如果程序内存溢出,Android系统只会kill掉该进程,而不会影响其他进程的使用(如果是system_process等系统进程出问题的话,则会引起系统重启)。

    二、出现内存泄露原因
    1.资源对象没关闭造成的内存泄露,try catch finally中将资源回收放到finally语句可以有效避免OOM。资源性对象比如:
    1-1,Cursor
    1-2,调用registerReceiver后未调用unregisterReceiver()
    1-3,未关闭InputStream/OutputStream
    1-4,Bitmap使用后未调用recycle()

    2.作用域不一样,导致对象不能被垃圾回收器回收,比如:
    2-1,非静态内部类会隐式地持有外部类的引用,
    2-2,Context泄露
    概括一下,避免Context相关的内存泄露,记住以下事情:
       1、 不要保留对Context-Activity长时间的引用(对Activity的引用的时候,必须确保拥有和Activity一样的生命周期)
       2、尝试使用Context-Application来替代Context-Activity 3、如果你不想控制内部类的生命周期,应避免在Activity中使用非静态的内部类,而应该使用静态的内部类,并在其中创建一个对Activity的弱引用。
          这种情况的解决办法是使用一个静态的内部类,其中拥有对外部类的WeakReference。
    2-3,Thread 引用其他对象也容易出现对象泄露。
    2-4,onReceive方法里执行了太多的操作
    3.内存压力过大
      3-1,图片资源加载过多,超过内存使用空间,例如Bitmap 的使用
      3-2,重复创建view,listview应该使用convertview和viewholder

    三、如何避免内存泄露
    1.使用缓存技术,比如LruCache、DiskLruCache、对象重复并且频繁调用可以考虑对象池
    2.对于引用生命周期不一样的对象,可以用软引用或弱引用SoftReferner WeakReferner
    3.对于资源对象 使用finally 强制关闭
    4.内存压力过大就要统一的管理内存

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

    推荐阅读更多精彩内容