Android内存泄漏原因分析

Posted 逆水当行舟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android内存泄漏原因分析相关的知识,希望对你有一定的参考价值。

内存泄漏的基本概念

什么是内存泄漏?

内存泄露就是:本应该被回收的对象,没有被回收。

内存泄露会产生什么影响?

  1. 系统分配给应用程序的内存资源是一定的,比如就是:50M
  2. 对象创建需要消耗内存资源,比如一个对象创建消耗了2M内存
  3. 发生内存泄露,2M内存没有回收,剩下的内存空间只有48M了,如果这种情况高频次发生
  4. 首先App会因为可用内存变少,而变得卡顿
  5. 还有可能因为内存泄露严重,造成运行时异常(OutOfMemeryError),直观反映就是应用在使用中直接崩溃

内存泄漏,泄漏具体在内存那里?

栈区

在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。
在一段代码定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域的事后,Java会自动释放掉为该变量所分配的内存空间。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限,线程之间不共享。

堆区

堆区用来存放new创建的对象和数组,线程共享。由程序员自己创建,但是在销毁的时候C/C++通过free()、delete()但是Java中必须依赖虚拟机GC,手动执行System.gc()只能提醒虚拟机执行GC,但是由于GC线程的优先级在虚拟机里实在太低,所以并不能保证其一定会执行。

方法区

又称静态区,跟堆区一样,被所有线程共享。用来存放常量,静态变量,以及代码。
方法区随类加载器加载。这里着重说静态变量,思考两个问题
- 使用静态变量用来保存数据和做线程通信是否可取?
- 方法区是否存在内存泄漏?

首先我们分析下静态变量的生命周期:

  1. 静态变量在类被加载的时候分配内存。
    哪什么时候,类被加载?
    当我们启动一个app的时候,系统会创建一个进程,此进程会加载一个Dalvik VM的实例,然后代码就运行在DVM之上,类的加载和卸载,垃圾回收等事情都由DVM负责。也就是说在进程启动的时候,类被加载,静态变量被分配内存

  2. 静态变量在类被卸载的时候销毁。
    那类又在什么时候被卸载?
    一般情况下,所有的类都是默认的ClassLoader加载的,只要ClassLoader存在,类就不会被卸载,而默认的ClassLoader生命周期是与进程一致的,我们不考虑程序修改类加载的情况,所以在进程结束的时候静态变量销毁

  3. android中的进程什么时候结束?
    这个是Android对进程和内存管理不同于PC的核心——如果资源足够,Android不会杀掉任何进程,这是否说明进程绝对安全,当然不是。这恰恰说明进程随时可能会被杀掉,因为设备资源是一定的,甚至可以说是稀缺的。
    而Android会在资源够的时候,重启被杀掉的进程。例如后台进程被杀死,这时候我们手动把后台进程切换到前台,App又会重建。但是这时候,我们的静态变量会在上次进程被杀死的时候被回收,也就是说静态变量的值,如果不做处理,是不可靠的,可以说内存中的一切都不可靠。如果要可靠,还是得保存到Nand或SD卡中去,在重启的时候恢复回来。具体操作可以参考Android-Application被回收引发空指针异常分析(消灭全局变量)

现在回到我们的问题

使用静态变量用来保存数据和做线程通信是否可取?
不可取,如果一定要这样做注意线程重建的时候恢复数据

方法区是否存在内存泄漏?
常常看到新人喜欢在静态变量中保存对象,用来做线程通信和存储App数据,这是不可取的。
静态变量的生命周期==App进程结束时间-App进程创建时间。也就是它会存在于整个App的生命周期,最开始我们给内存泄漏下了一个定义:本应该被回收的对象没有被回收就是内存泄漏。例如Activity A和Activity B之间的通信对象obj,他明显只需要存在于Activity A或者Activity B两个任意一个中,现在存在在了整个App,这符合内存泄漏的定义,属于内存泄漏。当然方法区保存的是对象的引用,真正的内存泄漏还是发生在堆区,所以从这个角度来看,也可以说方法区没有内存泄漏。
只要静态变量没有被销毁也没有置null,其对象一直被保持引用,也即引用计数不可能是0,所以不会被垃圾回收。因此,基于这点单例对象在运行时不会被回收

堆栈的区别

public class Test 
    public Test test = new Test();
    private int num;
    public void methodTest()
        int testNum = 1;
        Test test2 = new Test();
    

上面的代码中
在栈中的有:testNum、test2(持有Test的引用)
在堆中的有:num、test(持有Test的引用)、test所指向的对象,test2所指向的对象
局部变量的基本数据类型和对象的引用存在于栈中,他们随着方法的结束而结束
成员变量全部储存在堆中,包括基本数据类型、对象实体、对象引用,他们通过虚拟机来管理。

什么是引用

前面一直提到引用,那么什么是引用。
我们已经知道堆区存储对象,哪怎么找到这个对象,这就是引用,引用就是用来找到这个对象的具体位置的。对象存储在内存中他肯定会有一块内存区域,找到这块区域的首地址,首地址+对象的大小(sizeOf(obj))就是他的内存区域。举例来说

class A
A a = new A();

就可以说a持有A的引用,或者说a是A的引用
这时候定义class B

class B
    public A a = new A();

通过B.a可以拿到A对象,也可以说B持有A的引用。

为什么会发生内存泄漏?

我们已经知道内存泄漏发生在堆区,而且堆区的内存对象不能由程序员来手动释放,只能通过GC来完成,在搞清楚为什么会发生内存泄漏之前,先看看GC的工作原理和流程。
GC为了保证准确的释放对象,必须对对象进行监控,包括对象的创建、赋值、引用、被引用,从程序的运行对象开始搜索(运行栈、静态对象),运行对象和他们的引用对象组成的引用链,组成了无法回收的对象集合。其他的对象就是需要回收的。如下图所示:

当然,GC的种类也有很多,平缓的回收,中断式的,分层回收机制(新生代、次生带、幸存区、终身保障区)等等… 但是上层程序员并不需要关心这个。比如上图所表示的,如果object1,2,3,4中不小心持有了object5的引用,内存泄漏就这样发生了。

有什么办法可以修正它

首先是我们的编码经验和技巧,例如不要再单例中持有Activity,使用了数据库要关闭游标…等等。
其次因为GC过程与对象的引用类型是严重相关的
我们来看看Java对引用的分类Strong reference, SoftReference, WeakReference, PhatomReference。所以可以通过改变引用类型,灵活使用

类型表示用途回收策略
强引用A a = new A()默认类型Java宁愿抛出OOM也不会回收它
软引用SoftReference<Object>构建二级高速缓存内存不足时回收
弱引用WeakReference<Object>构建一级高速缓存GC后回收
虚引用-幽灵引用PhantomReference<Object>跟踪对象,查看其是否被回收GC后回收

- 软引用的用法:

    private void testSoftRef() 

//        创建对象 obj持有Object的强引用
        Object obj = new Object();
//        sr持有Object的软引用
        SoftReference<Object> sr = new SoftReference<Object>(obj);
//        去除强引用
        obj = null;
//        gc不一定执行 执行了也需要考虑内存是否不足再回收Object对象
        System.gc();
//        获取Object对象
        sr.get();

    
  • 弱引用
    和软引用使用一样
        WeakReference<Object> wr = new WeakReference<Object>(obj);
  • 虚引用 需要配合应用队列使用ReferenceQueue
    private void testSoftRef() 
//        创建对象 obj持有Object的强引用
        Object obj = new Object();
//        引用队列
        ReferenceQueue<Object> ref = new ReferenceQueue<>();
        PhantomReference<Object> wr = new PhantomReference<>(obj, ref);
//        去除强引用
        obj = null;
//        gc不一定执行 
        System.gc();
//        获取Object对象
        wr.get();
        ref.poll();
    

譬如图片缓存技术会经常在App中用到,用来减少流量和加速加载速度。我们可以通过SoftReference来构建一个图片缓存池。

public class CacheBySoftRef 
    // 首先定义一个HashMap,保存软引用对象。
    private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();
    // 再来定义一个方法,保存Bitmap的软引用到HashMap。
    public void addBitmapToCache(String path) 
        // 强引用的Bitmap对象
        Bitmap bitmap = BitmapFactory.decodeFile(path);
        // 软引用的Bitmap对象
        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
        // 添加该对象到Map中使其缓存
        imageCache.put(path, softBitmap);
    
    // 获取的时候,可以通过SoftReference的get()方法得到Bitmap对象。
    public Bitmap getBitmapByPath(String path) 
        // 从缓存中取软引用的Bitmap对象
        SoftReference<Bitmap> softBitmap = imageCache.get(path);
        // 判断是否存在软引用
        if (softBitmap == null) 
            return null;
        
        // 通过软引用取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空 ,如果未被回收,则可重复使用,提高速度。
        Bitmap bitmap = softBitmap.get();
        return bitmap;
    

注意:这里只是探讨避免内存泄漏的用法,因为

因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠

换句话说并不会等到内存快要满了,才清除回收软引用,而是这个策略变得模糊性更强
所以一般项目中还是使用划分APP可用内存块的1/4或者1/8来构建一个缓存池,使用强引用持有Bitmap对象。

以上是关于Android内存泄漏原因分析的主要内容,如果未能解决你的问题,请参考以下文章

Android内存泄漏

Android技术分享|Android 中部分内存泄漏示例及解决方案

Android技术分享| Android 中部分内存泄漏示例及解决方案

如何防止java中的内存泄漏

Android内存泄漏的检测流程捕捉以及分析

Android 内存泄漏