天天看点

一次内存泄露的分析及总结

概要

项目也使用LeakCanary一段时间了,也确实检测出了内存泄露,只是碍于项目进度,一直没有去进行分析。现在有时间了,就开始着手进行分析。LeakCanary泄露如下:

一次内存泄露的分析及总结

欢迎页泄露了26M,可怕吧。

过程

  1. 首先需要找出泄露的根源,LeakCanary也提供了这方面的帮助,点击泄露的项进去如下所示:
    一次内存泄露的分析及总结
    可以发现是线程导致的内存泄露,但是提供的信息还太小,所以接下来需要使用一个更高级的工具-MAT(Memory Analyzer), MAT需要使用到hprof文件,当然贴心的LeakCanary也提供了这方面的支持,点击图中的Share heap dump,既可以生成hprof文件,不过安卓生成的hprof文件不能直接被MAT识别,需要进入到android sdk中的platform-tools通过命令行进行转换:

    hprof-conv before.hprof format.hprof

    ,除了这种方式之外,也可以通过android studio自带的Android Profiler工具生成hprof文件,进入Android Profiler,进入Memory模式,接着点击图中红色框框中的按钮进行dump,之后会dump一段时间然后进行分析,如下
    一次内存泄露的分析及总结
    最后,只要点击红色框的保存按钮,即可以保存一份hprof文件,当然这个也需要经过上面的命令转换。
    一次内存泄露的分析及总结
  2. 接下来使用MAT工具,打开转换后的文件,如下:
    一次内存泄露的分析及总结
    然后点击框中的按钮,进入domiator_tree分析,然后在编辑框搜索WelcomeActivity,搜索结果如下:
    一次内存泄露的分析及总结
    ,接着右键Path To GCRoots,并且除去其他的弱引用等。
    一次内存泄露的分析及总结
    得到的结果如下图,可以发现线程的来源于第三方服务——极光推送
    一次内存泄露的分析及总结
    接着进入极光推送的jar包进行查看cn.jiguang.d.d.q这个类的变量a,如下
    一次内存泄露的分析及总结
    ,到这里真相终于水落石出,q这个类是Runnable的实现并且持有了WelcomeActivity的context引用,由于线程的异步执行导致了WelcomeActivity即使销毁,context仍然被持有从而泄露。LeakCancary提到的泄露线程的名字是pool-11-thread-1, 我们可以查出这个引用这个线程的线程池就可以得到相关的信息了,如下
    一次内存泄露的分析及总结
    ,线程池的名字前缀是pool-11-thread-, 而threadNumber = 2是先赋值再增,所以赋值给泄露线程的值是1,所以可知泄露线程的名字是pool-11-thread-1。
  3. 总结如下,由于在WelcomeActivity中调用了极光的服务,而极光的服务持有了他的context并且赋值给线程,因此导致了context无法被回收而导致内存泄露。

总结

   首先需要了解LeakCanary的原理,可以参考这篇文章LeakCanary原理浅析,原理大致如下:LeakCanary在活动A销毁的时候,进行观察,给活动A加上了弱引用WeakReference还有ReferenceQueue。ReferenceQueue有这样一个特性,如果WeakReference持有的类被回收了,就会进入ReferenceQueue队列里面,我们只需要让队列出队,如果出队的类存在,说明这个类没有其他的GcRoots所引用,即可认为该类已经回收不存在泄露。如果类泄露了,那么LeakCanary会再进行dump heap的分析,得到泄露的路径。

   最后,我想到了一个问题,如果持有context的线程已经结束了,那么内存还能释放吗?这里就需要使用到GcRoots的相关知识了,那么GcRoots都有哪些呢,如下:

常说的GC(Garbage Collector) roots,特指的是垃圾收集器(Garbage Collector)的对象,GC会收集那些不是GC roots且没有被GC roots引用的对象。

一个对象可以属于多个root,GC root有几下种:

Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.

Thread - 活着的线程

Stack Local - Java方法的local变量或参数

JNI Local - JNI方法的local变量或参数

JNI Global - 全局JNI引用

Monitor Used - 用于同步的监控对象

Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于”JVM持有”的了。

所以对于线程来说,重点是活着的线程,如果线程已经执行结束了,已经不是GcRoots并且没有其他引用,那么线程也会被回收,从而被引用的context也能得到释放,内存也能得到回收,这个我也有进行,内存确实能够得到回收。但是对于LeakCanary而言,它关注的是活动销毁时的状态,它并没有跟踪活动之后的状态,所以LeakCanary的最大作用就是给我们提个醒,让我们早点发现和规避内存的泄露。

参考

Mat内存优化

LeakCanary原理浅析

GcRoots