欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android中的内存泄露

程序员文章站 2024-01-24 22:16:47
...

内存泄露是造成内存溢出的重要原因之一。Android的内存模型基于jvm的基本实现。底层依赖可达性算法来回收对象。JVM对每个对象状态的监控给我们带来了便利,虽然造成了一定程度上性能的损失。为了让大家能更明白内存泄露的本质,文章会从java的内存模型讲起,最终举出几个内存泄露的例子和解决方案。最重要的是,理解了原理,你将会举一反三,内存泄露将变得异常简单。

java运行时内存模型

Android中的内存泄露

Android中的内存泄露

回收算法

  • JVM回收算法主要有两种
    • 引用计数法:每个对象有一个引用计数器,当对象被引用一次时计数器加一,引用失效计数器减一。当计数器为0时表示对象可以被回收。(由于无法解决相互引用问题而被废弃)
    • 可达性算法:从GC ROOT节点开始遍历,可以连通的对象都是活对象。无法到达的对象可以被回收。
      • 可以作为GC ROOT节点的对象
        • 虚拟机栈的栈帧的局部变量表引用的对象
        • 本地方法栈JNI引用的对象
        • 方法区的静态变量和常量所引用的对象

例子

public class GCDemo {
    public static void main(String[] args) {
        GcObject obj1 = new GcObject();
        GcObject obj2 = new GcObject();

        obj1.instance = obj2;
        obj2.instance = obj1;

        obj1 = null;
        obj2 = null;
    }
}

class GcObject {
    public Object instance = null;
}

引用计数法内存图

Android中的内存泄露

  • step1:GcObject实例1的引用计数+1,目前为1
  • step2:GcObject实例2的引用计数+1,目前为1
  • step3:GcObject实例2的引用计数+1,目前为2
  • step4:GcObject实例1的引用计数+1,目前为2
  • obj1 = null:GcObject实例1引用计数-1,目前为1
  • obj2 = null:GcObject实例2引用计数-1,目前为1
  • 到此为止,GcObject实例1和2都无法使用,但是引用计数不为0,发生内存泄露。

可达性算法内存图

Android中的内存泄露

  • 由可以被作为GC ROOT对象来看,虚拟机栈中obj1和obj2是两个GC ROOT起点,由于最终都将obj1与obj2设置为了null。因此GcObject1和2都无法可达,因此可以被回收。

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

Android中常见的内存泄露

Android中最常见的内存泄露是关于Activity的内存泄漏。其核心问题在于在Activity生命周期之外仍有其引用。从内存模型角度来讲,它在GC ROOTING时可达,但是它的onDestroy已经被执行。既它从我们期待的行为上来说应该被标识为可以被回收。但它仍然可达。以下就列出最常见的几种情况,请记住核心矛盾:Activity生命周期之外仍有其引用

Handler导致的内存泄漏

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);

    // 延时10分钟发送一个消息
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { }
    }, 60 * 10 * 1000);

    // 返回前一个Activity
    finish();
  }
}
  • 思考上面代码,它会发生严重的内存泄露。首先,mLeakyHandler被声明为了一个匿名内部类(自己思考,上述代码有几个匿名内部类),它隐式的持有了外部类Activity的强引用。之后,handler发出了一个10分钟的延时消息,接着Activity杀掉了自己。问题的关键在于handler发出的消息Message会在消息队列里存i在10分钟。它持有发出消息的handler的引用。而handler又持有Activity的强引用。这就导致Activity在其生命周期之外仍有强引用,发生了严重的内存泄露。

  • 注意,问题的核心在于内部类以及匿名内部类都会隐式的持有外部类的强引用。这就导致一个类的生命不在由一个因素控制,变为了多个因素。在我们认为该结束的时候而没有产生正确的行为。解决的方案很简单,也很通用:

    • 用静态内部类。
    • 在静态内部类根据需求使用弱引用修饰需要引用的外部类资源。
public class SampleActivity extends Activity {
    /**
    * 匿名类的静态实例不会隐式持有他们外部类的引用
    */
    private static final Runnable sRunnable = new Runnable() {
            @Override
            public void run() {
            }
        };

    private final MyHandler mHandler = new MyHandler(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 延时10分钟发送一个消息.
        mHandler.postDelayed(sRunnable, 60 * 10 * 1000);

        // 返回前一个Activity
        finish();
    }

    /**
    * 静态内部类的实例不会隐式持有他们外部类的引用。
    */
    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) {
                // ...
            }
        }
    }
}

static变量导致的内存泄漏

static变量在进程启动的时候被分配内存空间。在进程被杀死的时候释放内存空间。因此,它的生命周期是贯穿整个应用的生命周期的。也就是说,当你的Activity被静态变量染指到。那么又会导致Activity在其生命周期之外仍有强引用,发生内存泄露。

  • 常见的场景如下:
    • static Context
    • static View
  • 两者的本质是一样的,因为View内部持有Context的引用。第一种最常见的就是单例模式的实现,将具体的Activity的Context传递进去构造单例对象,导致泄露。第二种是当View需要加载的资源比较大时,想一劳永逸。而产生内存泄露。
  • 解决方法依然通用且简单,在Activity声明周期外使用Context尽量去使用Application的Context
  • Activiity内部声明的静态资源要及时释放, 在相应的声明周期方法中做好收尾工作

注册与解注册

  • 在andorid开发中,我们经常会在Activity的onCreate中注册广播接受器、EventBus等,如果忘记成对的使用反注册,可能会引起内存泄漏。开发过程中应该养成良好的相关,在onCreate或onResume中注册,要记得相应的在onDestroy或onPause中反注册。

创建与关闭

在android中,资源性对象比如Cursor、File、Bitmap、视频等,系统都用了一些缓冲技术,在使用这些资源的时候,如果我们确保自己不再使用这些资源了,要及时关闭,否则可能引起内存泄漏。因为有些操作不仅仅只是涉及到Dalvik虚拟机,还涉及到底层C/C++等的内存管理,不能完全寄希望虚拟机帮我们完成内存管理。
在这些资源不使用的时候,记得调用相应的类似close()、destroy()、recycler()、release()等函数,这些函数往往会通过jni调用底层C/C++的相应函数,完成相关的内存释放。

  • Cursor对象及时关闭
  • IO对象及时关闭

集合造成的内存泄漏

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;   
}
  • 画个简单的内存图就可以看出在栈中分配的v指向了在堆中分配的十个空间,虽然每次置为null,但是将V作为GC ROOT的起点仍然可达每一个在堆中的Object对象,而我们本意是将每一个Object对象置为了null。
  • 解决方案很简单:v = null 。本质上是断掉GC ROOT的起点。