Android面试系列文章2018之内存管理之内存泄漏篇

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ClAndEllen/article/details/79382815

Android面试系列文章2018之内存管理之内存泄漏篇

1.Java内存泄漏基础知识

  内存泄漏的理解其实很简单,就是该被释放的对象没有被释放,一直被某个或者某些实例引用所持有,导致不能被垃圾回收。

1)Java内存的分配策略:

  a.静态存储区(方法区):跟堆一样,被所有的线程共享。方法区包含所有的class和static变量,方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。

  b.栈区:每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中,每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问,栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

  c.堆区:动态内存分配,new出来的对象都存放在此区域,没用的对象都会被GC进行回收。存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令) ,jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身

2.Java是如何管理内存的

这里写图片描述

  Java的内存管理就是对象的分配和释放问题。(两部分)

  分配 :内存的分配是由程序完成的,程序员需要通过关键字new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。

  释放 :对象的释放是由垃圾回收机制决定和执行的,这样做确实简化了程序员的工作。但同时,它也加重了JVM的工作。因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。

3.Java当中的内存泄漏

  在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连(也就是说仍存在该内存对象的引用);其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。简单来说就是无用对象(不再使用的对象)持续占有内存或者无用对象得不到及时的释放,从而造成内存空间的浪费叫内存泄漏。

4.Android中的内存泄漏

[1]单例模式的使用:不恰当的使用单例会造成内存泄漏。当你的某个单例对象持有某个Activity的上下文的时候,这回导致这个Activity无法被回收的。如下图所示:

这里写图片描述

正确的写法应该是这样的:

这里写图片描述

[2]匿名内部类

  内部类在Java中是一个很常见的数据结构。它们很受欢迎,因为它们可以以这样的方式来定义:即只有外部类可以实例化它们。很多人可能没有意识到的是这样的类会持有外部类的隐式引用。隐式引用很容易出错,尤其是当两个类具有不同的生命周期。以下是常见的Android Activity写法。

public class AsyncActivity extends Activity {

    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async);
        textView = (TextView) findViewById(R.id.textView);

        new BackgroundTask().execute();
    }

    private class BackgroundTask extends AsyncTask<Void, Void, String> {

        @Override
        protected String doInBackground(Void... params) {
            // Do background work. Code omitted.
            return "some string";
        }

        @Override
        protected void onPostExecute(String result) {
            textView.setText(result);
        }
    }
}

  这种特殊的实现在执行上没有问题。问题是,它保留内存的时间肯定会超过必要的时间。由于BackgroundTask持有一个AsyncActivity隐式引用并运行在另一个没有取消策略的线程上,它将保留AsyncActivity在内存中的所有资源连接,直到后台线程终止运行。在HTTP请求的情况下,这可能需要很长的时间,尤其是在速度较慢的连接。

  匿名类和内部类有同样的缺点,即他们持有外部类的引用。如同内部类,一个匿名类在Activity生命周期之外执行或在其他线程执行工作时,可能会导致内存泄漏。在这个例子中,我将使用流行的HTTP请求库Retrofit执行API调用,并传递响应给对应回调。根据Retrofit homepage上面例子对Retrofit进行配置。我会在Application中持有GitHubService引用,这不是一个特别好的设计,这仅仅服务于这个例子的目的。

public class ListenerActivity extends Activity {

    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listener);
        textView = (TextView) findViewById(R.id.textView);

        GitHubService service = ((LeaksApplication) getApplication()).getService();
        service.listRepos("google")
                .enqueue(new Callback<List<Repo>>() {
                    @Override
                    public void onResponse(Call<List<Repo>> call,
                                           Response<List<Repo>> response) {
                        int numberOfRepos = response.body().size();
                        textView.setText(String.valueOf(numberOfRepos));
                    }

                    @Override
                    public void onFailure(Call<List<Repo>> call, Throwable t) {
                        // Code omitted.
                    }
                });
    }
}

  这是常见的解决方案,不应该导致任何泄漏。但是,如果我们在慢速连接中执行这个例子,分析结果会有所不同。请记住,直到该线程终止,该Activity会一直被持有,就像在内部类的例子。

[3]Handler

  如果你在某个Activity的内部写了如下有关Handler的代码,那么就会导致内存泄漏问题:

这里写图片描述

  这种写法导致内存泄漏的原因就是因为这是匿名内部类的方式创建Handler对象,这个匿名内部类是持有外部Activity的引用的,导致外部Activity在应该被回收的时候无法被回收,从而导致了内存泄漏问题。那么正确的写法应该如何写呢?如下图所示:

这里写图片描述

  自定义一个继承Handler的类,同时声明它是静态的,,并且在生成实例对象的时候,应该持有外部Activity的弱引用。

[4]尽量避免使用static变量。

[5]资源未关闭造成的内存泄漏。

[6]AsyncTask造成的内存泄漏。它的原因和Handler比较相似,我这里就不啰嗦了。

[7]Bitmap在C区的内存泄漏问题,使用recycle方法即可对C区的内存进行回收,从而避免内存泄漏。

没有更多推荐了,返回首页