新年过后献上关于Android内存泄漏的种种总结

Android 内存泄漏总结

内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问 题。内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一 直被某个或某些实例所持有却不再被使用导致 GC 不能回收 我会从 java 内存泄漏的基础知识开始,并通过具体例子来说明 Android 引起内存泄 漏的各种原因,以及如何利用工具来分析应用内存泄漏,最后再做总结。 篇幅有些长,大家可以分几节来看!

顺手留下GitHub链接,需要获取相关面试等内容的可以自己去找
https://github.com/xiangjiana/Android-MS
(VX:mm14525201314)

Java 内存分配策略

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

  • 静态存储区(方法区): 主要存放静态数据、全局 static 数据和常量。这块内 存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
  • 栈区 :当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结 束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于 处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。 这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。
栈与堆的区别:

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

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

举个例子:

  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,而它自己存在于栈中。

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

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

了解了 Java 的内存分配之后,我们再来看看 Java 是怎么管理内存的。

Java是如何管理内存

Java的内存管理就是对象的分配和释放问题。在 Java 中,程序员需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空 间。另外,对象的释放是由 GC 决定和执行的。在 Java 中,内存的分配是由程序 完成的,而内存的释放是由 GC 完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是 Java 程序运行速度较慢的原因 之一。因为,GC 为了能够正确释放对象,GC 必须监控每一个对象的运行状态, 包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。

监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该 对象不再被引用。

为了更好理解 GC 的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系 考虑为图的有向边,有向边从引用者指向被引对象。另外,每个线程对象可以作为 一个图的起始顶点,例如大多程序从 main 进程开始执行,那么该图就是以 main 进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象, GC将不回收这些对象。如果某个对象 (连通子图)与这个根顶点不可达(注意,该图 为有向图),那么我们认为这个(这些)对象不再被引用,可以被 GC 回收。 以下,我 们举一个例子说明如何用有向图表示内存管理。对于程序的每一个时刻,我们都有 一个有向图表示JVM的内存分配情况。以下右图,就是左边程序运行到第6行的示 意图。

Java使用有向图的方式进行内存管理,可以消除引用循环的问题,例如有三个对 象,相互引用,只要它们和根进程不可达的,那么GC也是可以回收它们的。这种 方式的优点是管理内存的精度很高,但是效率较低。另外一种常用的内存管理技术 是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行 低(很难处理循环引用的问题),但执行效率很高。

什么是Java中的内存泄露

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

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可 达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对 象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。

通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序 员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java提高了编 程的效率。

因此,通过以上分析,我们知道在Java中也有内存泄漏,但范围比C++要小一些。 因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。

对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访 问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义, 该函数不 保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法 管理GC。通常,GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有 的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执 行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特 定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网 络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要 调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系 列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。

同样给出一个 Java 内存泄漏的典型例子:

  Vector v = new Vector(10);
  for (int i = 1; i < 100; i++) {
        Object o = new Object();
        v.add(o);
        o = null;
  }

在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个 Vector 中, 如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除, 最简单的方法就是将 Vector 对象设置为 null。

Android中常见的内存泄漏汇总

  • 集合类泄漏
    集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占 用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等 即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集 合所占用的内存只增不减。比如上面的典型例子就是其中一种情况,当然实际 上我们在项目中肯定不会写这么 2B 的代码,但稍不注意还是很容易出现这种 情况,比如我们都喜欢通过 HashMap 做一些缓存之类的事,这种情况就要多 留一些心眼。
  • 单例造成的内存泄漏
    由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用 不恰当的话,很容易造成内存泄漏。比如下面一个典型的例子,
    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;
     }
    }

    这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context, 所以这个Context的生命周期的长短至关重要:

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

正确的方式应该改为下面这种方式:

  public class AppManager {
      private static AppManager instance;
      private Context context;
      private AppManager(Context context) {
         this.context = context.getApplicationContext();// 使用Applica tion 的context
      }
      public static AppManager getInstance(Context context) {
        if (instance == null) {
           instance = new AppManager(context); }return instance;
      }
   }

或者这样写,连 Context 都不用传进来了:

  在你的 Application 中添加一个静态方法,getContext() 返回 Application 的 context,
  ...
  context = getApplicationContext();
  ...
     /**
        * 获取全局的context
        * @return 返回全局context对象
        */
     public static Context getContext(){
         return context;
     }
  public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager() {
      this.context = MyApplication.getContext();// 使用Application 的context
    }
    public static AppManager getInstance() {
      if (instance == null) {
        instance = new AppManager();
    }
    return instance;
    }
  }
  • 匿名内部类/非静态内部类和异步线程

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

    有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数 据资源,可能会出现这种写法:

    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的应用场景如下:

其中: 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使用了匿名内部类。我们来看看运行时这两个引用的内 存:

可以看到,ref1没什么特别的。 但ref2这个匿名类的实现对象里面多了一个引用:
this$0这个引用指向MainActivity.this,也就是说当前的MainActivity实例会被ref2持 有,如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时 候,就造成了Activity的泄露。

  • Handler 造成的内存泄漏
    Handler 的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了 避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回 调等api都借助Handler来处理,但 Handler不是万能的,对于 Handler 的使用 代码编写一不规范即有可能造成内存泄漏。另外我们知道 Handler、 Message 和 MessageQueue都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程MessageQueue 一直持有。 由于 Handler 属于TLS(Thread Local Storage) 变 量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。
    举个例子:

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

    在该 SampleActivity中声明了一个延迟10分钟执行的消息 MessagemLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activityfinish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成 内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

修复方法: 在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为 静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,代码省略....
综述,即推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判 空。前面提到了 WeakReference,所以这里就简单的说一下 Java 对象的几种引用类 型。Java对引用的分类有 Strong reference,SoftReference, WeakReference, PhatomReference 四种。

未完待续....
顺手留下GitHub链接,需要获取相关面试等内容的可以自己去找
https://github.com/xiangjiana/Android-MS
(VX:mm14525201314)

原文地址:https://blog.51cto.com/14541311/2469167

时间: 2024-09-28 21:34:27

新年过后献上关于Android内存泄漏的种种总结的相关文章

关于Android内存泄漏的种种总结第二弹

衔接上篇:新年过后献上关于Android内存泄漏的种种总结(顺手留下GitHub链接,需要获取相关面试等内容的可以自己去找)https://github.com/xiangjiana/Android-MS(VX:mm14525201314) 在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周 期较长的对象时候,可以尽量应用软引用和弱引用技术. 软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用 的对象被垃圾回收器回收,Java虚拟机

android 内存泄漏分析技巧

java虚拟机运行一般都有一个内存界限,超过这个界限,就会报outofmemory.这个时候一般都是存在内存泄漏.解决内存泄漏问题,窃以为分为两个步骤:分析应用程序是否真的有内存泄漏,找到内存泄漏的地方.这两个步骤都不是一般意义上的调试,直接打log,断点调试都不是太给力.动脑筋想一想,内存问题应该在很多地方上都会出现,这么常见的问题应该是有工具的.android现在更可以说是一个生态系统,当然也有很多开发辅助工具.在前面的两个步骤中都有很强大的武器,熟练的掌握这些利器,分析问题就会事半功倍.

android内存泄漏系列- 分析hprof文件

转载请注明出处 http://www.cnblogs.com/weiwangnuanyang/p/5703702.html ,谢谢. 如果只是想确定一下某一个场景是否有内存泄漏,AndroidStadio的控制台就有一个好工具,反复操作观察曲线是否上扬,如果曲线上扬则说明内存泄漏 但是,上面的工具不够强大,不能看出内存中驻留的具体的类和类的引用关系. 下面就来重点介绍一下,解决android内存泄漏必备利器-Memory Analysis; 具体安装方式请移步度娘. 我们这里重点介绍如何利用Me

Android内存泄漏

韩梦飞沙  韩亚飞  [email protected]  yue31313  han_meng_fei_sha #Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题.内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用导致 GC 不能回收.最近自己阅读了大量相关的文档资料,打算做个 总结 沉淀下来跟大家一起分享和学习,也给自己一个警示,以后 coding 时怎么避免这些情况,提高应用的体验

Android内存泄漏的各种原因详解

转:http://mobile.51cto.com/abased-406286.htm 1.资源对象没关闭造成的内存泄漏 描述: 资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存.它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外.如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏.因为有些资源性对象,比如 SQLiteCursor(在析构函数finalize(),如果我们没有关

Android 内存泄漏优化汇总

android内存泄漏优化摘要 博客分类: android android内存溢出OutOfMemoryError . android移动应用程序的内存分配一般是8凯瑟琳约,不正确地假定处理内存处理非常easy创建OutOfMemoryError.我们的产品是最常见的错误是OutOfMemoryError的异常, 在解决这个异常时在网上发现非常多关于OutOfMemoryError的原因的介绍. OutOfMemoryError主要由下面几种情况造成: 1.数据库的cursor没有关闭. 操作S

android 内存泄漏检测工具 LeakCanary 泄漏金丝雀

韩梦飞沙 yue31313 韩亚飞 han_meng_fei_sha [email protected] 内存泄漏检测工具 android 内存泄漏检测工具 ======== 内存泄漏 就是  无用的对象没有被回收,占用着内存,使得可用内存变小了. 如何检测内存泄漏, 可以使用 LeakCanary来检测内存泄漏. leak  是 泄漏的意思.. Canary 是 金丝雀 的意思. 在运行 应用的时候, 泄漏金丝雀 如果检测到内存泄漏 会显示一个通知. ======== LeakCanary捕获

Android内存泄漏查找和解决

Android内存泄漏查找和解决 目录: 内存泄漏的概念 一个内存泄漏的例子 Java中"失效"的private修饰符 回头看内存泄漏例子泄漏的重点 强引用与弱引用 解决内部类的内存泄漏 Context造成的泄漏 使用LeakCanary工具查找内存泄漏 总结 一.内存泄漏概念 1.什么是内存泄漏? 用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元.直到程序结束.即所谓的内存泄漏. 其实说白了就是该内存空间使用完毕之后未回收 2.内存泄漏会导致的问题 内

Android 性能篇 -- 带你领略Android内存泄漏的前世今生

基础了解 什么是内存泄漏? 内存泄漏是当程序不再使用到的内存时,释放内存失败而产生了无用的内存消耗.内存泄漏并不是指物理上的内存消失,这里的内存泄漏是指由程序分配的内存但是由于程序逻辑错误而导致程序失去了对该内存的控制,使得内存浪费. Java 内存分配策略 Java 程序运行时的内存分配策略有三种,分别是 静态分配 . 栈式分配 和 堆式分配 ,对应的三种存储策略使用的内存空间主要分别是 静态存储区(也称方法区) . 栈区 和 堆区 . ?? 静态存储区(方法区):主要存放 静态数据 . 全局