Android 内存泄漏

前言

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。有些内存泄漏是很难发现的,需要使用恰当的方法或者辅助工具才能检测到,这篇文章记一下 Android 应用程序中如何检测内存泄漏。

一、java 虚拟机运行时数据区域

java 虚拟机在执行 java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域有着各自的创建时间和销毁时间,各自用途也不一样。java虚拟机运行时数据区域如下图:
Android 内存泄漏

1.1 程序计数器

程序计数器是一块较小的内存空间,是当前线程所执行的字节码的行号指示器。如果线程正在执行一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果是Naive方法,则计数器为空;这个区域不会出现OUtOfMemoryError异常。此区域也是唯一一个在java虚拟机规范中没有规定任何OUtOfMemoryError情况的区域。
java虚拟机多线程是使用线程轮流切换并分配处理执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。为了线程切换后能够恢复到正确的执行位置,每条线程都需要一套独立的线程计数器,这些计数器之间相互独立,独立存储,这个内存区域为“线程私有”。

1.2 java 虚拟机栈

java虚拟机栈也是线程私有,与线程的生命周期一致,在执行每个方法都会创建一个Stack Frame。每一个方法从开始执行到结束,对应一个Stack Frame在虚拟机值栈中从入栈和出栈的过程。如果线程请求的栈深度大于虚拟机所允许的深度,就会出现*Exception。如果允许动态扩展,在扩展的过程中,如果无法申请到足够的内存,则会抛出OutOfMemoryException异常。

1.3 本地方法栈

和java虚拟机栈的作用类似,不同点在本地方法栈主要是为虚拟机使用到的Native方法提供服务,本地方法栈也会抛出*Exception和OutOfMemoryException异常。

1.4 java 堆

堆是java虚拟机中内存中最大的一块,被所有线程共享的一块内存区域,在虚拟机创建时创建。作用就是存放对象实例,所有的对象的实例都需要在这里分配内存。几乎所有的对象实例和对象数组都需要在堆上分配。是java虚拟机内存回收的管理的重要区域,因此也被称为“GC”堆,可以被分为:新生代和老年代;Eden空间、From Survivor空间、To Survivor空间。如果堆中没有内存完成实例分配,并且堆也无法扩展时,则抛出OutOfMemoryException异常。

1.5 方法区

方法区和java堆一样,是各个线程共享的内存区域,用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据。通常被开发人员成为“永久带”。这个区域的内存回收的目标就是针对常亮池的回收和对类型的卸载,也是较为难处理的部分。如果方法区的内存空间不满足内存分配需求时,Java虚拟机会抛出OutOfMemoryError异常

1.6 运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。它用来存放编译时期生成的字面量和符号引用,这些内容会在类加载后存放在方法区的运行时常量池中。运行时常量池可以理解为是类或接口的常量池的运行时表现形式。当创建类或接口时,如果构造运行时常量池所需的内存超过了方法区所能提供的最大值,Java虚拟机会抛出OutOfMemoryError异常。

二、java 四种引用类型

java 堆中存储的都是引用类型的数据,而 java 对象存在四种引用类型,分别是强引用、软引用、弱引用和虚引用。
Java中提供这四种引用类型主要有两个目的:
第一是可以让程序员通过代码的方式决定某些对象的生命周期;
第二是有利于JVM进行垃圾回收。

下面来阐述一下这四种类型引用的概念:

2.1 强引用

强引用类型是我们平时写代码的时候最常用的引用,指创建一个对象并把这个对象赋给一个引用变量。刚学习java的人都会忽略这个概念,成一种理所当然的事情了,比如:

Class Apple{}

Apple apple =new Apple();

如果某个对象被强引用所引用,这个引用又被其他对象所持有,那么JVM 宁愿抛出 OutOfMemory 错误也不会回收这种对象。

2.2 软引用

如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它;

如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性。
SoftReference的特点是它的一个实例保存对一个Java对象的软引用, 该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。

也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对 这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。

另外,一旦垃圾线程回收该Java对象之 后,get()方法将返回null。

举个例子:

Class Apple{}

Apple apple = new Apple();
SoftReference<Apple> softRef = new SoftReference<Apple>(apple);

此时这个 Apple 对象有两个引用,一个是强引用 apple ,一个是软引用 softRef,若想该对象只被软引用引用,只需将强引用和该对象的连接断开。

apple = null;

在该对象被回收之前,我们也可通过 get 方法获得该对象,再声明一个强引用指向它即可:

Apple a = softRef.get();

此时 a 即是该对象的强引用。如果该对象已经被回收,那么 softRef.get() 将返回 null。

SoftReference 有两个构造方法:

public SoftReference(T referent) {
        super(referent);
        this.timestamp = clock;
    }


public SoftReference(T referent, ReferenceQueue<? super T> q) {
    
}

作为一个Java对象,SoftReference对象除了具有保存软引用的特殊性之外,也具有Java对象的一般性。所以,当软可及对象被回收之后,虽然这个SoftReference对象的get()方法返回null,但这个SoftReference对象已经不再具有存在的价值,需要一个适当的清除机制,避免大量SoftReference对象带来的内存泄漏。在java.lang.ref包里还提供了ReferenceQueue。如果在创建SoftReference对象的时候,使用了一个ReferenceQueue对象作为参数提供给SoftReference的构造方法,那么当这个SoftReference所软引用的aMyOhject被垃圾收集器回收的同时,ref所强引用的SoftReference对象被列入ReferenceQueue。也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的 Reference 对象。另外从 ReferenceQueue 这个名字也可以看出,它是一个队列,当我们调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。
在任何时候,我们都可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。利用这个方法,我们可以检查哪个SoftReference所软引用的对象已经被回收。于是我们可以把这些失去所软引用的对象的SoftReference对象清除掉。

2.3 弱引用

弱引用,就是引用与对象之间的联系很弱,弱到垃圾回收器会无视这个引用,直接回收对象。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用也可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

Class Apple{}

Apple apple = new Apple();
WeakReference<Apple> weakRef = new WeakReference<Apple>(apple);
apple = null;

2.4 虚引用

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

三 举例分析 java 堆中的内存泄漏

一般 java 中所说的内存泄漏指的是 java 堆中所产生的内存泄漏,java 堆中内存是由 java 虚拟机的“GC”所管理的,java 虚拟机实时监控到一些对象不可到达,即在以后的程序中不会被用到,触发 GC 时,这样的对象将会被回收掉,其所占的内存也将释放。从对象的生命周期角度来讲,如果一个长生命周期的对象持有了一个短生命周期对象的引用,将导致这个短生命周期的对象无法回收,其所占的内存释放不了,就会有内存泄漏的风险。

3.1 内存泄漏举例分析 —— Handler 使用不当造成的内存泄漏

Android 中使用 Handler 造成内存泄漏的代码:

public class MainActivity extends AppCompatActivity {

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // ...
    }
}

上面是一段简单的Handler的使用。当使用内部类(包括匿名类)来创建Handler的时候,Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用(不然你怎么可能通过Handler来操作Activity中的View?)
而Handler通常会伴随着一个耗时的后台线程(例如从网络拉取图片)一起出现,这个后台线程在任务执行完毕(例如图片下载完毕)之后,通过消息机制通知Handler,然后Handler把图片更新到界面。然而,如果用户在网络请求过程中关闭了Activity,正常情况下,Activity不再被使用,它就有可能在GC检查时被回收掉,但由于这时线程尚未执行完,而该线程持有Handler的引用(不然它怎么发消息给Handler?),这个Handler又持有Activity的引用,就导致该Activity无法被回收(即内存泄露),直到网络请求结束(例如图片下载完毕)。另外,如果你执行了Handler的postDelayed()方法,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,那么在你设定的delay到达之前,会有一条MessageQueue -> Message -> Handler -> Activity的链,导致你的Activity被持有引用而无法被回收。

3.2 内存泄漏的检测

3.2.1 集成 LeakCanary 框架

LeakCanary 开发者应该都会使用,不熟悉的新同学可以参考 LeakCanary 中文使用说明

3.2.2 使用 Android Profiler 检测

使用内存分析器来执行以下操作:

  • 在可能导致性能问题的时间轴中寻找不良的内存分配模式
  • Dump Java堆,以便在任何时间查看哪些对象正在使用内存。长时间的堆转储可以帮助识别内存泄漏。
  • 在正常和极端的用户交互过程中记录内存分配,以精确地确定您的代码在短时间内分配的对象或分配被泄漏的对象。
    内存分析器概述:
    Android 内存泄漏

如上图所示,内存分析器的默认视图包括以下内容:

① 强制执行垃圾收集事件的按钮。
② 捕获堆转储的按钮。
③ 记录内存分配的按钮。
④ 放大时间线的按钮。
⑤ 跳转到实时内存数据的按钮。
⑥ 事件时间线显示活动状态、用户输入事件和屏幕旋转事件。
⑦ 内存使用时间表,其中包括以下内容:

  • 每个内存类别使用多少内存的堆栈图,如左边的y轴和顶部的颜色键所示。
  • 虚线表示已分配对象的数量,如右侧y轴所示。
  • 每个垃圾收集事件的图标。

查看堆转储时,查看分配了多少内存的快照很有用,它不会显示如何分配内存。为此,您需要记录内存分配。完成记录会话后,您可以看到以下记录的持续时间:

分配了哪些对象以及它们使用了多少空间。
在堆栈跟踪中分配每个对象的位置,其中包括线程。

要查看应用程序的内存分配,请单击内存分析器工具栏中的Record memory allocations。当它记录时,与你的应用程序进行交互,以引起内存溢出或内存泄漏。完成后,单击Stop recording。

要检查分配记录,请按照下列步骤操作:

  • 浏览列表以查找具有非常大的堆计数且可能泄漏的对象,要帮助查找已知类,请单击类名列标题按字母顺序排序。然后单击一个类名,Instance View 窗格就会显示在右侧,显示该类的每个实例,如下图所示。
  • 在Instance View窗格中,单击一个实例。Call Stack选项卡显示在下面,显示了哪个实例被分配在哪个线程中。
  • 在Call Stack选项卡中,单击任意行可以在编辑器中跳转到该代码。
    Android 内存泄漏

默认情况下,列表是按类名排列的。在列表的顶部,您可以使用右下拉菜单在列表之间切换:

  • Arrange by class: 根据类名分配。
  • Arrange by package:根据包名分配。
  • Arrange by callstack: 根据调用堆栈排序

要捕获堆转储,单击Memory-Profiler工具栏中的dump Java堆,然后分析结合上面选项分析,针对上述示例,结果如下图:
Android 内存泄漏

分析结果已经很明确了。
上面示例比较简单,针对复杂的问题,我们还可以使用 eclipse-MAT 工具加以分析。

3.2 利用软引用和弱引用解决OOM问题

根据前面讲了关于软引用和弱引用相关的基础知识,那么到底如何利用它们来优化程序性能,从而避免OOM的问题呢?
下面举个例子,假如有一个应用需要读取大量的本地图片,如果每次读取图片都从硬盘读取,则会严重影响性能,但是如果全部加载到内存当中,又有可能造成内存溢出,此时使用软引用可以解决这个问题。
设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。在Android开发中对于大量图片下载会经常用到。

针对上述例子,首先我们把匿名内部类 Handler 声明为静态内部类,这样Handler类中就不持有 Activity 的引用了。但是,这样在 Handler 中要使用 activity 对象,则通过弱引用来解决这个问题。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // ...
    }

    private static class NoLeakHandler extends Handler {
        
        //持有弱引用MainActivity,GC回收时会被回收掉.
        private WeakReference<MainActivity> mActivity;

        public NoLeakHandler(MainActivity activity) {
            mActivity = new WeakReference<>(activity);
        }
 
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }
}

当然针对 Handler 的内存泄漏,我们也可以通过程序来解决:

  1. 在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。
  2. 如果你的Handler是被delay的Message持有了引用,那么使用相应的Handler方法:removeCallbacks(Runnable r)和removeMessages(int what),在页面销毁时把消息对象从消息队列移除就行了。
@Override
public void onDestroy() {
   // 移除所有消息
   handler.removeCallbacksAndMessages(null);
   // 或者移除单条消息
   handler.removeMessages(what);
}

Android 内存泄漏

上一篇:springboot中application.properties配置


下一篇:IIS部署网站出现Server Error in '/' Application.CS0016的解决办法