内存泄露及LeakCanary原理

224 阅读6分钟

1.什么是OOM?什么是内存泄露?什么是内存抖动?

内存溢出(OOM):系统会给每个App分配内存就是Heap Size值。当App占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存时就会抛出Out of Memory异常。

内存泄露:当一个对象不再使用了,本应该被垃圾回收器(JVM)回收,但是这个对象由于被其他正在使用的对象所持有,造成无法回收的结果。内存泄漏最终就会导致内存溢出(OOM)。

内存抖动:是指在短时间内有大量的对象被创建或者被回收的现象,主要是循环中大量创建、回收对象。

这三者的重要等级分别:内存溢出(OOM)> 内存泄露 > 内存抖动

2.常见的造成内存泄漏的几种场景

①单例造成的内存泄漏:单例是长时间存活的,如果持有Activity的引用,那么就会导致Activity在销毁的时候不会被回收;

②线程造成的内存泄漏:在Activity内部启动匿名的Thread,在Thread内部执行耗时的操作,Thread不退出的话,就会导致Activity在销毁的时候不会被回收;

③非静态内部类创建静态实例造成的内存泄漏:非静态内部类持有外部类的引用 (在kotlin中,内部类只有在使用外部类的对象、方法、变量等才会持有外部类的引用,否则不会持有),那么就会导致Activity在销毁的时候不会被回收;

④Handler造成的内存泄露:原因如上,Handler持有外部类的引用,Message持有Handler的引用,MessageQueue持有Message的引用,Looper持有MessageQueue的引用,Looper一直存在,导致引用链都不会被回收;

⑤属性动画造成的内存泄漏:属性动画有一类无限循环的动画,如果在Activity中播放此类动画且没有在onDestory中去停止动画,那么动画会一直播放下去,尽管在界面已经看不到动画效果了,这个时候Activity的View会被动画持有,而View又持有了Activity,最终Activity无法释放。

以上就是常见的导致内存泄漏的操作,那么系统GC在做垃圾回收的时候会根据一定的规则判断对象是否能被回收,那规则是什么呢?

java对象是存活还是死亡,判断对象是死亡才会进行回收,而判断对象是否存活有2种方法:引用计数法和可达性分析法;

当前最常见的是可达性分析法,从字面上来看,该方法的判断需要一个链头,然后一步一步向下搜索,如果可达,代表不能回收;如果不可达,代表可以回收。这个链头就是GC Roots。

image.png

关于GC Roots和垃圾回收机制的原理请移步juejin.cn/post/708589…

LeakCanary准备知识

1.1 Reference

Java中的四种引用类型,我们先简单复习下

  • 强引用,对象有强引用时不能被回收
  • 软引用 SoftReference,对象只有软引用时,在内存不足时触发GC会回收该对象
  • 弱引用 WeakReference,对象只有弱引用时,下次GC就会回收该对象
  • 虚引用 PhantomReference,平常很少会用到,源码注释主要用来监听对象清理前的动作,比Java finalization更灵活,PhantomReference 需要与 ReferenceQueue 一起配合使用。

Reference 主要是负责内存的一个状态,当然它还和java虚拟机,垃圾回收器打交道。Reference 类首先把内存分为4种状态 Active,Pending,Enqueued,Inactive。

  • Active 一般来说内存一开始被分配的状态都是 Active,
  • Pending 大概是指快要被放进队列的对象,也就是马上要回收的对象,
  • Enqueued 就是对象的内存已经被回收了,我们已经把这个对象放入到一个队列中,方便以后我们查询某个对象是否被回收,
  • Inactive 就是最终的状态,不能再变为其它状态。

1.2 ReferenceQueue

引用队列,当检测到对象的可达性更改时,垃圾回收器将已注册的引用对象添加到队列中

ReferenceQueue实现了入队(enqueue)和出队(poll),还有remove操作,内部元素head就是泛型的Reference,并且Queue的实现,是由Reference自身的链表结构( 单向循环链表 )所实现的。

1.3 简单例子

当我们想检测一个对象是否被回收了,那么我们就可以采用 Reference + ReferenceQueue,大概需要几个步骤:

  1. 创建一个引用队列 queue
  2. 创建 Reference 对象,并关联引用队列 queue
  3. 在 reference 被回收的时候,Reference 会被添加到 queue 中
//创建一个引用队列  
ReferenceQueue queue = new ReferenceQueue();  
  
// 创建弱引用,此时状态为Active,并且Reference.pending为空
WeakReference reference = new WeakReference(new Object(), queue);  
System.out.println(reference);
System.out.println(reference.isEnqueued());
// 当GC执行后,由于是弱引用,所以回收该object对象,并且置于pending上,此时reference的状态为PENDING  
System.gc();  

/* ReferenceHandler从pending中取下该元素,并且将该元素放入到queue中,此时Reference状态为ENQUEUED,Reference.queue = ReferenceENQUEUED */  
System.out.println(reference);
System.out.println(reference.isEnqueued());

/* 当从queue里面取出该元素,则变为INACTIVE,Reference.queue = Reference.NULL */  
Reference reference1 = queue.remove();  
System.out.println(reference1);
System.out.println(reference1.isEnqueued());

打印信息:

image.png

reference.enqueue():将此引用对象添加到其注册的队列(如果有)。如果此引用对象已成功入队,则为true;如果它已入队或创建时未向队列注册,则为false

reference.isEnqueued():告诉此引用对象是否已由程序或垃圾收集器排队。如果此引用对象在创建时未向队列注册,则此方法将始终返回false,仅当此引用对象已排队时为true。

查看reference源码可以发现,初始化WeakReference reference = new WeakReference(new Object(), queue); 是不会将引用对象添加到其注册的队列中,只有触发GC,在ReferenceHandler中才会将reference添加到队列中,这个过程在ReferenceHandler中完成

还需注意的是触发GC先将reference的状态从Active变为Pending,只有在ReferenceHandler内部调用了reference.enqueue()才会变为Enqueued状态,当reference从其注册的queue移除后状态又变为Inactive。

那这个可以用来干什么了?

可以用来检测内存泄露,leakCanary 就是采用这种原理来检测的。

  • 监听 Activity 的生命周期
  • 在 onDestroy 的时候,创建相应的 Reference 和 ReferenceQueue,并启动后台进程去检测
  • 一段时间之后,从 ReferenceQueue 读取,若读取不到相应 activity 的 Reference,有可能发生泄露了,这个时候,进行GC,一段时间之后(默认5s),再去读取,若在从 ReferenceQueue 还是读取不到相应 activity 的 Reference,可以断定是发生内存泄露了
  • 发生内存泄露之后,dump,分析 hprof 文件,找到泄露路径

这是根据原理一个大致的流程,具体LeakCanary流程见下方:

image.png

1.ObjectWatcher创建了一个KeyedWeakReference来监视对象

2.稍后,在后台线程中,延时检查引用是否已被清除,如果没有则触发GC

3.如果引用一直没有被清除,它会dumps the heap到一个.hprof文件中,然后将.hprof文件存储到文件系统

4.分析过程主要在HeapAnalyzerService中进行,Leakcanary2.0以后使用Shark来解析.hprof文件

5.HeapAnalyzer获取.hprof中的所有KeyWeakReference并获取objectId

6.HeapAnalyzer计算objectId到GC Roots的最短强引用链路来确定是否有泄漏,然后构建导致泄漏的引用链

7.将分析结果存储在数据库中,并显示泄露通知