JVM内存管理概述与android内存泄露分析

 一.内存划分

将内存划分为六大部分,分别是PC寄存器、JAVA虚拟机栈、JAVA堆、方法区、运行时常量池以及本地方法栈.
1、PC寄存器(线程独有):全称是程序计数寄存器,它记载着每一个线程当前运行的JAVA方法的地址,
如果是当前执行的是本地方法,则程序计数器会是一个空地址。它的作用就是用来支持多线程,线程的阻塞、恢复、
挂起等一系列操作,直观的想象一下,要是没有记住每个线程当前运行的位置,又如何恢复呢。依据这一点,
每一个线程都有一个PC寄存器,也就是说PC寄存器是线程独有的。

2、JAVA虚拟机栈(线程独有):JAVA虚拟机栈是在创建线程的同时创建的,用于存储栈帧,
JAVA虚拟机栈也是线程独有的。

3、JAVA堆(全局共享):这一部分是JAVA内存中最重要的一部分,之所以说是最重要的一部分,
并不是因为它的重要性,而是指作为开发人员最应该关注的一部分。它随着JAVA虚拟机的启动创建,
储存着所有对象实例以及数组对象,而且内置了“自动内存管理系统”,也就是我们常说的垃圾搜集器(GC)。
JAVA堆中的内存释放是不受开发人员控制的,完全由JAVA虚拟机一手操办。对于JAVA虚拟机如何实现垃圾搜集器,
JAVA虚拟机规范没有明确的规定,也正因如此,我们平时使用的JAVA虚拟机中提供了许多种垃圾搜集器,
它们采用不同的算法以及实现方式,已满足多方面的性能需求。

4、方法区(全局共享):方法区也是堆的一个组成部分,它主要存储的是运行时常量池、字段信息、方法信息、
构造方法与普通函数的字节码内容以及一些特殊方法。它与JAVA堆的区别除了存储的信息与JAVA堆不一样之外,
最大的区别就是这一部分JAVA虚拟机规范不强制要求实现自动内存管理系统(GC)。

5、本地方法栈(线程独有):本地方法栈是一个传统的栈,它用来支持native方法的执行。
如果JAVA虚拟机是使用的其它语言实现指令集解释器的时候,也会用到本地方法栈。如果前面这两种都未发生,
也就是说如果JAVA虚拟机不依赖于本地方法栈,而且JAVA虚拟机也不支持native方法,则不需要本地方法栈。
而如果需要的话,则本地方法栈也是随每一个线程的启动而创建的。

上面五个内存区域,除了PC寄存器之外,其余四个一般情况下,都要求JAVA虚拟机实现提供给客户调节大小的参数,
也就是我们常用的Xms、Xmx等等。

Java 内存分配策略

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

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

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

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

 

四种引用类型的介绍

  1. 强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;

  2. 软引用(SoftReference):只有在内存空间不足时,才会被回的对象;

  3. 弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;

  4. 虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

我们常说的内存泄漏是指new出来的Object无法被GC回收,即为强引用:  

什么是Java中的内存泄露

对于C++来说,内存泄漏就是new出来的对象没有delete,俗称野指针;对于Java来说,就是new出来的Object 放在Heap上无法被GC回收;  

在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中常见的内存泄漏汇总 

什么是内存溢出:(Out Of Memory,简称 OOM),通俗理解就是内存不够,即内存占用超出内存的空间大小;基本上会造成程序奔溃。解决:找到报错的代码并解决。
什么是内存泄露:内存泄漏(Memory Leak),简单理解就是内存使用完毕之后本该垃圾回收却未被回收。(占着茅坑不拉屎)--内存无法回收又没起作用。

    

推理!!!
1.看对象的大小;
2.深入使用MAT进行对象细节的分析
3.前后对比。
怀疑某一段代码会造成内存溢出。可以先注册掉该段代码生成一个xxx.hroph文件,然后在解开这段代码再生成一个yyy.hroph文件
通过MAT工具对比他们的内存消耗情况---甚至可以精确到每一个对象消耗内存的情况。

 

 在 Android 中,泄露 Context 对象的问题尤其严重,特别像 Activity 这样的 Context 对象会引用大量很占用内存的对象,如果 Context 对象发生了内存泄漏,那它所引用的所有对象都被泄漏了。Activity 是非常重量级的对象,所以我们应该极力避免妨碍系统对其进行回收,然而实际情况是有多种方式会无意间就泄露了Activity 对象。

case 0.静态变量造成的内存泄漏

 最简单的泄漏 Activity 就是在 Activity 类中定义一个 static 变量,并将其指向一个运行中的 Activity 实例。如果在 Activity 的生命周期结束之前,没有清除这个引用,那它就会泄漏。由于 Activity 的类对象是静态的,一旦加载,就会在 APP 运行时一直常驻内存,如果类对象不卸载,其静态成员就不会被垃圾回收。

尽量避免使用 static 成员变量:
这里修复的方法是:
不要在类初始时初始化静态成员。可以考虑lazy初始化。

case 1. 单例造成的内存泄露

单例的静态特性导致其生命周期同应用一样长

另一种类似的情况是对经常启动的 Activity 实现一个单例模式,让其常驻内存可以使它能够快速恢复状态。

    如我们有一个创建起来非常耗时的 View,在同一个 Activity 不同的生命周期中都保持不变呢,就为它实现一个单例模式。一旦 View 被加载到界面中,它就会持有 Context 的强引用,也就是我们的 Activity 对象。

    由于我们是通过一个静态成员引用了这个 View,所以我们也就引用了 Activity,因此 Activity 就发生了泄漏。所以一定不要把加载的 View 赋值给静态变量,如果你真的需要,那一定要确保在 Activity 销毁之前将其从 View 层级中移除。

解决方案:

  1. 将该属性的引用方式改为弱引用;

  2. 如果传入Context,使用ApplicationContext;

case 2. InnerClass匿名内部类

我们经常在 Activity 内部定义一个内部类,这样做可以增加封装性和可读性。但是如果当我们创建了一个内部类的对象,并通过静态变量持有了 Activity 的引用,那也会可能发生 Activity 泄漏。

在Java中,非静态内部类 和 匿名类 都会潜在的引用它们所属的外部类,但是,静态内部类却不会。如果这个非静态内部类实例做了一些耗时的操作,就会造成外围对象不会被回收,从而导致内存泄漏。

解决方案:

  1. 将内部类变成静态内部类;

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

  3. 如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用;

  4. 在业务允许的情况下,当Activity执行onDestory时,结束这些耗时任务;

case 3. 线程造成的内存泄漏

 在 Activity 内定义了一个匿名的 AsyncTask 对象,就有可能发生内存泄漏。如果 Activity 被销毁之后 AsyncTask 仍然在执行,那就会阻止垃圾回收器回收Activity 对象,进而导致内存泄漏,直到执行结束才能回收 Activity。

    同样的,使用 Thread 和 TimerTask 也可能导致 Activity 泄漏。只要它们是通过匿名类创建的,尽管它们在单独的线程被执行,它们也会持有对 Activity 的强引用,进而导致内存泄漏。

在Android里面线程最容易造成内存泄露。线程产生内存泄露的主要原因在于线程生命周期的不可控
2.线程问题的改进方式主要有:
   1)将线程的内部类,改为静态内部类。
   2)在程序中尽量采用弱引用保存Context。

case 4. Activity Context 的不正确使用

在Android应用程序中通常可以使用两种Context对象:Activity和Application。当类或方法需要Context对象的时候常见的做法是使用第一个作为Context参数。这样就意味着View对象对整个Activity保持引用,因此也就保持对Activty的所有的引用。

假设一个场景,当应用程序有个比较大的Bitmap类型的图片,每次旋转是都重新加载图片所用的时间较多。为了提高屏幕旋转是Activity的创建速度,最简单的方法时将这个Bitmap对象使用Static修饰。 当一个Drawable绑定在View上,实际上这个View对象就会成为这份Drawable的一个Callback成员变量。而静态变量的生命周期要长于Activity。导致了当旋转屏幕时,Activity无法被回收,而造成内存泄露。

解决方案:

  1. 使用ApplicationContext代替ActivityContext,因为ApplicationContext会随着应用程序的存在而存在,而不依赖于activity的生命周期;

  2. 对Context的引用不要超过它本身的生命周期,慎重的对Context使用“static”关键字。Context里如果有线程,一定要在onDestroy()里及时停掉。

case 5. Handler引起的内存泄漏

定义一个匿名的 Runnable 对象并将其提交到 Handler 上也可能导致 Activity 泄漏。Runnable 对象间接地引用了定义它的 Activity 对象,而它会被提交到Handler 的 MessageQueue 中,如果它在 Activity 销毁时还没有被处理,就会导致 Activity 泄漏。

当Handler中有延迟的的任务或是等待执行的任务队列过长,由于消息持有对Handler的引用,而Handler又持有对其外部类的潜在引用,这条引用关系会一直保持到消息得到处理,而导致了Activity无法被垃圾回收器回收,而导致了内存泄露。

修复方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:

Handler 的持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message和 Runnable.
综述,即推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。

解决方案:

  1. 可以把Handler类放在单独的类文件中,或者使用静态内部类便可以避免泄露;

  2. 如果想在Handler内部去调用所在的Activity,那么可以在handler内部使用弱引用的方式去指向所在Activity.使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的。

case 6. 注册监听器的泄漏(资源未关闭造成的内存泄漏)

如系统服务可以通过 context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。如果 Context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有 Activity 的引用,如果开发者忘记在 Activity 销毁时取消注册,也会导致 Activity泄漏

系统服务可以通过Context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。如果Context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有Activity 的引用,如果在Activity onDestory时没有释放掉引用就会内存泄漏。

解决方案:

  1. 使用ApplicationContext代替ActivityContext;

  2. 在Activity执行onDestory时,调用反注册;

case 7. Cursor,Stream没有close,View没有recyle

资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null. 在我们的程序退出时一定要确保我们的资源性对象已经关闭。

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

Solution:

调用onRecycled()

case 8. 集合中对象没清理造成的内存泄漏

我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

解决方案:

在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序。

Solution

private List<EmotionPanelInfo> data;    
public void onDestory() {        
   if (data != null) {        data.clear();        data = null;    } }

case 9. WebView造成的泄露

当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露。

解决方案:

为webView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

case 10. 构造Adapter时,没有使用缓存的ConvertView

初始时ListView会从Adapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象 缓存起来。
当向上滚动ListView时,原先位于最上面的List Item的View对象会被回收,然后被用来构造新出现的最下面的List Item。
这个构造过程就是由getView()方法完成的,getView()的第二个形参View ConvertView就是被缓存起来的List Item的View对象(初始化时缓存中没有View对象则ConvertView是null)。

 

case 11.动画

在属性动画中有一类无限循环动画,如果在Activity中播放这类动画并且在onDestroy中去停止动画,那么这个动画将会一直播放下去,这时候Activity会被View所持有,从而导致Activity无法被释放。解决此类问题则是需要早Activity中onDestroy去去调用objectAnimator.cancel()来停止动画

case 12.第三方库使用不当

对于EventBus,RxJava等一些第三开源框架的使用,若是在Activity销毁之前没有进行解除订阅将会导致内存泄漏。

 

综上所述,要避免内存泄露或者内存溢出,主要要遵循以下几点:

第一:不要为Context长期保存引用(要引用Context就要使得引用对象和它本身的生命周期保持一致,即对activity的引用应该控制在activity的生命周期之内)。

第二:如果要使用到Context,尽量使用ApplicationContext去代替Context,因为ApplicationContext的生命周期较长,引用情况下不会造成内存泄露问题

第三:在你不控制对象的生命周期的情况下避免在你的Activity中使用static变量。尽量使用WeakReference去代替一个static。

第四:垃圾回收器并不保证能准确回收内存,这样在使用自己需要的内容时,主要生命周期和及时释放掉不需要的对象。尽量在Activity的生命周期结束时,在onDestroy中把我们做引用的其他对象做资源释放,比如:cursor.close()。如清空对图片等资源有直接引用或者间接引用的数组(使用array.clear();array = null);

第五:尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用。如果使用静态内部类,将外部实例引用作为弱引用持有。
(静态的内部类不会持有外部类的一个隐式引用)

在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap 后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ; array = null)等,最好遵循谁创建谁释放的原则。
正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。
保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。

 

程序出现停止运行状态或者致命崩溃现象可能如下原因导致:

1.在while死循环里面或软件操作非常频繁的代码块中进行new对象产生强引用对象,
导致jvm不能及时回收内存,从而产生内存消耗暴增,让软件出现停止运行的致命崩溃现象

2.代码报错未捕捉异常造成

3.程序在主线程耗时过长,或者在广播中的耗时超过6s

 

new出来的对象不用时回收原则;

1.在哪创建就在哪及时释放,
2.谁引用,谁就负责释放
能做到C/C++对于程序的“谁创建,谁释放”原则,那我们对于内存的把握,并不比Java或Android本身的GC机制差,而且更好的控制内存,能使我们的手机运行得更流畅

java没有绝对的强制垃圾回收的方法,不过可以这样去做:
1. 对于不再引用的对象,及时把它的引用赋为null。 obj = null;
2. 如果内存确实很紧张,调用System.gc() 方法来建议垃圾回收器开始回收垃圾。

 

参考文章

以上部分图片、实例代码和文段都摘自或参考以下文章 : 
IBM : 
Java的内存泄漏

Android Design Patterns : 
How to Leak a Context: Handlers & Inner Classes

伯乐在线团队: 
Android性能优化之常见的内存泄漏

我厂同学 : 
Dalvik虚拟机 Finalize 方法执行分析

腾讯bugly : 
内存泄露从入门到精通三部曲之基础知识篇

LeakCanary : 
LeakCanary 中文使用说明 
LeakCanary: 让内存泄露无所遁形

https://github.com/square/leakcanary

 

posted @ 2017-05-21 11:36  一汪海洋  阅读(467)  评论(0编辑  收藏  举报