love li fang fang forever

Android内存泄漏分析与解决办法-持新

• Theory

这篇文章是因为今天我的小团子告诉我她的手机”微信“无响应了,后来经过我的了解,就是当收到“19 。。。。”(19.数字后面为15个句号.字符),手机会出现卡死现象,作为Android开发者都知道这个是ANR的问题,这顿时勾起了我的兴趣,然后我就跟她专业性的分析什么是ANR,之后讲到内存泄漏会引起内存溢出的问题,也决定以此为契机写一篇文章来谈谈Android内存泄漏,这篇文章也算是欠下很久了吧。以下有截图。

微信ANR的问题是在Android 6.5.13版本上发现,华为,三星,OPPO,魅族纷纷中招,IOS设备和小米手机上暂时没有发现此BUG。

ANR : Android官方规定不允许在UI线程执行耗时的操作,如请求网络资源,下载图片等,超过5秒系统则会造成手机卡死,系统会弹出(Application Not Response)对话框,建议耗时任务都放在后台子线程中去处理。

微信团队官方答复:Android针对文字排版功能进行了优化,为了在显示的时对齐文本排版美观,在此耗时的过程中意外出现的BUG。

后来我查看日志(LOG)信息有这么一个异常信息:

java.util.regex.Matcher.
findNextImpl(Nativemethod)

然后猜测是在UI线程做了正则匹配的操作(目的文字排版功能进行优化),导致主线程阻塞,不同的手机硬件配置不一,UI的渲染能力不同,这也解释了小米手机在硬件上面还是有不错的表现。


ANR真没什么可说的,以这个为契入点,文章会一步一步的探讨内存泄漏问题。

JAVA是垃圾回收语言的一种,开发者无需特意管理内存分配。但是JAVA中还是存在着许多内存泄露的可能性,如果不好好处理内存泄露,会导致APP内存单元无法释放被浪费掉,最终导致内存全部占据堆栈(heap)挤爆进而程序崩溃。

内存泄漏

说到内存泄露,就不得不提到内存溢出OOM,这两个比较容易混淆的概念。

内存泄露:程序在向系统申请分配内存空间后(New),在使用完毕后未释放。结果导致一直占据该内存单元,我们和程序都无法再使用该内存单元,直到程序结束,这是内存泄露。  

内存溢出(OutOfMemory):程序向系统申请的内存空间超出了系统能给的。比如内存只能分配一个int类型,我却要塞给他一个long类型,double类型,系统就出现内存溢出,不够用的情况。

大量的内存泄漏会导致内存溢出。

内存

想要了解内存泄露,对内存的了解必不可少

Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,JAVA是在JVM所虚拟出的内存环境中运行的,对应JVM的内存也可分为三个区:堆(heap)、栈(stack)和方法区(method)。

静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

栈区(stack) :当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放,栈是简单的数据结构,但在计算机中使用广泛。栈最显著的特征是:LIFO(Last In, First Out, 后进先出)。比如我们往箱子里面放衣服,先放入的在最下方,只有拿出后来放入的才能拿到下方的衣服。栈中只存放基本类型和对象的引用(不是对象)。

堆区 (heap): 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。 JVM只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身。

内存的概念大概理解清楚后,要考虑的问题来了:到底是哪里的内存会让我们造成内存泄露?

内存泄露原因分析

在JAVA中JVM的栈记录了方法的调用,每个线程拥有一个栈。在线程的运行过程当中,执行到一个新的方法调用,就在栈中增加一个内存单元,即帧(frame)。在frame中,保存有该方法调用的参数、局部变量和返回地址。然而JAVA中的局部变量只能是基本类型变量(int),或者对象的引用。所以在栈中只存放基本类型变量和对象的引用。引用的对象保存在堆中。

当某方法运行结束时,该方法对应的frame将会从栈中删除,frame中所有局部变量和参数所占有的空间也随之释放。线程回到原方法继续执行,当所有的栈都清空的时候,程序也就随之运行结束。

而对于堆内存,堆存放着普通变量。在JAVA中堆内存不会随着方法的结束而清空,所以在方法中定义了局部变量,在方法结束后变量依然存活在堆中。

综上所述,栈(stack)可以自行清除不用的内存空间。但是如果我们不停的创建新对象,堆(heap)的内存空间就会被消耗尽。所以JAVA引入了垃圾回收(garbage collection,简称GC)去处理堆内存的回收,但如果对象一直被引用无法被回收,造成内存的浪费,无法再被使用。所以对象无法被GC回收就是造成内存泄露的原因!

垃圾回收机制

垃圾回收(garbage collection,简称GC)可以自动清空堆中不再使用的对象。在JAVA中对象是通过引用使用的。如果再没有引用指向该对象,那么该对象就无从处理或调用该对象,这样的对象称为不可到达(unreachable)。垃圾回收用于释放不可到达的对象所占据的内存。

实现思想:我们将栈定义为root,遍历栈中所有的对象的引用,再遍历一遍堆中的对象。因为栈中的对象的引用执行完毕就删除,所以我们就可以通过栈中的对象的引用,查找到堆中没有被指向的对象,这些对象即为不可到达对象,对其进行垃圾回收。

如果持有对象的强引用,垃圾回收器是无法在内存中回收这个对象。

引用类型

在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及(reachable)状态,程序才能使用它。从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。 Java/Android引用类型及其使用分析(现在2017年09月25 JAVA9 都已经发布了,大家可以关注一下)

1.强引用(Strong reference)
实际编码中最常见的一种引用类型。常见形式如:A a = new A();等。强引用本身存储在栈内存中,其存储指向对内存中对象的地址。一般情况下,当对内存中的对象不再有任何强引用指向它时,垃圾回收机器开始考虑可能要对此内存进行的垃圾回收。如当进行编码:a = null,此时,刚刚在堆中分配地址并新建的a对象没有其他的任何引用,当系统进行垃圾回收时,堆内存将被垃圾回收。

2.软引用(Soft Reference)
软引用的一般使用形式如下:

A a = new A();
SoftReference<A> srA = new SoftReference<A>(a);

软引用所指示的对象进行垃圾回收需要满足如下两个条件:
1.当其指示的对象没有任何强引用对象指向它。
2.当虚拟机内存不足时。

因此,SoftReference变相的延长了其指示对象占据堆内存的时间,直到虚拟机内存不足时垃圾回收器才回收此堆内存空间。

3.弱引用(Weak Reference)
同样的,软引用的一般使用形式如下:

A a = new A();
WeakReference<A> wrA = new WeakReference<A>(a);

WeakReference不改变原有强引用对象的垃圾回收时机,一旦其指示对象没有任何强引用对象时,此对象即进入正常的垃圾回收流程。

4.虚引用(Phantom Reference)
与SoftReference或WeakReference相比,PhantomReference主要差别体现在如下几点:
1.PhantomReference只有一个构造函数

PhantomReference(T referent, ReferenceQueue<? super T> q)

2.不管有无强引用指向PhantomReference的指示对象,PhantomReference的get()方法返回结果都是null。

因此,PhantomReference使用必须结合ReferenceQueue; 与WeakReference相同,PhantomReference并不会改变其指示对象的垃圾回收时机。

内存泄露原因

如果持有对象的强引用,垃圾回收器是无法在内存中回收这个对象。

内存泄露的真因是:持有对象的强引用,且没有及时释放,进而造成内存单元一直被占用,浪费空间,甚至可能造成内存溢出!

其实在Android中会造成内存泄露的情景无外乎两种:

全局进程(process-global)的static变量。这个无视应用的状态,持有Activity的强引用的怪物。 活在Activity生命周期之外的线程。没有清空对Activity的强引用。 检查一下你的项目中是否有以下几种情况:

Static Activities Static Views Inner Classes Anonymous Classes Handler Threads TimerTask Sensor Manager

详解见该文章《Android内存泄漏的八种可能》

最后推荐一个可检测app内存泄露的项目:LeakCanary(可以检测app的内存泄露)

未完待续~~~~~~~~~~~~~~~晴雨