java 源码系列 - 带你读懂 Reference 和 ReferenceQueue

https://blog.csdn.net/gdutxiaoxu/article/details/80738581

一步步拆解 LeakCanary

https://blog.csdn.net/gdutxiaoxu/article/details/80752876

从基础讲起

Reference

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

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

ReferenceQueue

引用队列,当检测到对象的可到达性更改时,垃圾回收器将已注册的引用对象添加到队列中,ReferenceQueue实现了入队(enqueue)和出队(poll),还有remove操作,内部元素head就是泛型的Reference。

简单例子

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

  • 创建一个引用队列 queue
  • 创建 Reference 对象,并关联引用队列 queue
  • 在 reference 被回收的时候,Reference 会被添加到 queue 中
创建一个引用队列
ReferenceQueue queue = new ReferenceQueue();  // 创建弱引用,此时状态为Active,并且Reference.pending为空,当前Reference.queue = 上面创建的queue,并且next=null
WeakReference reference = new WeakReference(new Object(), queue);
System.out.println(reference);
// 当GC执行后,由于是弱引用,所以回收该object对象,并且置于pending上,此时reference的状态为PENDING
System.gc();  /* ReferenceHandler从pending中取下该元素,并且将该元素放入到queue中,此时Reference状态为ENQUEUED,Reference.queue = ReferenceENQUEUED */  /* 当从queue里面取出该元素,则变为INACTIVE,Reference.queue = Reference.NULL */
Reference reference1 = queue.remove();
System.out.println(reference1);

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

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

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

Reference

主要内存成员变量

private T referent;
volatile ReferenceQueue<? super T> queue;  /* When active:   NULL *     pending:   this *    Enqueued:   next reference in queue (or this if last) *    Inactive:   this */
@SuppressWarnings("rawtypes")
Reference next;  transient private Reference<T> discovered;  /* used by VM */  /* List of References waiting to be enqueued.  The collector adds* References to this list, while the Reference-handler thread removes* them.  This list is protected by the above lock object. The* list uses the discovered field to link its elements.*/
private static Reference<Object> pending = null;
  • referent表示其引用的对象,即在构造的时候需要被包装在其中的对象。
  • queue 是对象即将被回收时所要通知的队列。当对象即将被回收时,整个reference对象,而不仅仅是被回收的对象,会被放到queue 里面,然后外部程序即可通过监控这个 queue 即可拿到相应的数据了。
  • next 即当前引用节点所存储的下一个即将被处理的节点。但 next 仅在放到queue中才会有意义,因为只有在enqueue的时候,会将next设置为下一个要处理的Reference对象。为了描述相应的状态值,在放到队列当中后,其queue就不会再引用这个队列了。而是引用一个特殊的 ENQUEUED(内部定义的一个空队列)。因为已经放到队列当中,并且不会再次放到队列当中。
  • discovered 表示要处理的对象的下一个对象。即可以理解要处理的对象也是一个链表,通过discovered进行排队,这边只需要不停地拿到pending,然后再通过discovered 不断地拿到下一个对象赋值给pending即可,直到取到了最有一个。它是被JVM 使用的。
  • pending 是等待被入队的引用列表。JVM 收集器会添加引用到这个列表,直到Reference-handler线程移除了它们。这个列表使用 discovered 字段来连接它下一个元素(即 pending 的下一个元素就是discovered对象。r = pending; pending = r.discovered)。

接下来,我们来看一下 Reference 的静态代码块

static {ThreadGroup tg = Thread.currentThread().getThreadGroup();for (ThreadGroup tgn = tg;tgn != null;tg = tgn, tgn = tg.getParent());Thread handler = new ReferenceHandler(tg, "Reference Handler");/* If there were a special system-only priority greater than* MAX_PRIORITY, it would be used here*/handler.setPriority(Thread.MAX_PRIORITY);handler.setDaemon(true);handler.start();// provide access in SharedSecretsSharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {@Overridepublic boolean tryHandlePendingReference() {return tryHandlePending(false);}});
}

我们,当 Reference 类被加载的时候,会执行静态代码块。在静态代码块里面,会启动 ReferenceHandler 线程,并设置线程的级别为最大级别, Thread.MAX_PRIORITY。

接下来我们来看一下 ReferenceHandler 这个类,可以看到 run 方法里面是一个死循环,我们主要关注 tryHandlePending 方法就 Ok 了

private static class ReferenceHandler extends Thread {----- // 核心代码如下public void run() {while (true) {tryHandlePending(true);}}
}
static boolean tryHandlePending(boolean waitForNotify) {Reference<Object> r;Cleaner c;try {synchronized (lock) {// 检查 pending 是否为 null,不为 null,制定 pending enqueueif (pending != null) {r = pending;// 'instanceof' might throw OutOfMemoryError sometimes// so do this before un-linking 'r' from the 'pending' chain...c = r instanceof Cleaner ? (Cleaner) r : null;// unlink 'r' from 'pending' chainpending = r.discovered;r.discovered = null;} else { // 为 null。等待// The waiting on the lock may cause an OutOfMemoryError// because it may try to allocate exception objects.if (waitForNotify) {lock.wait();}// retry if waitedreturn waitForNotify;}}} catch (OutOfMemoryError x) {// Give other threads CPU time so they hopefully drop some live references// and GC reclaims some space.// Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above// persistently throws OOME for some time...Thread.yield();// retryreturn true;} catch (InterruptedException x) {// retryreturn true;}// Fast path for cleanersif (c != null) {c.clean();return true;}ReferenceQueue<? super Object> q = r.queue;if (q != ReferenceQueue.NULL) q.enqueue(r);return true;
}

在 tryHandlePending 方法里面,检查 pending 是否为 null,如果pending不为 null,则将 pending 进行 enqueue,否则线程进入 wait 状态。

问题来了,我们从 Reference 源码中发现没有给 discovered和 pending 赋值的地方,那 pending和 discovered 到底是谁给他们赋值的。

我们回头再来看一下注释:简单来说,垃圾回收器会把 References 添加进入,Reference-handler thread 会移除它,即 discovered和 pending 是由垃圾回收器进行赋值的

/* List of References waiting to be enqueued.  The collector adds* References to this list, while the Reference-handler thread removes* them.  This list is protected by the above lock object. The* list uses the discovered field to link its elements.*/
private static Reference<Object> pending = null;

ReferenceQueue

接下来,我们在来看一下 ReferenceQueue 的 enqueue 方法

boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class */synchronized (lock) {// Check that since getting the lock this reference hasn't already been// enqueued (and even then removed)ReferenceQueue<?> queue = r.queue;// queue 为 null 或者 queue 已经被回收了,直接返回if ((queue == NULL) || (queue == ENQUEUED)) {return false;}assert queue == this;// 将 Reference 的状态置为 Enqueued,表示已经被回收r.queue = ENQUEUED;// 接着,将 Reference 插入到链表// 判断当前链表是否为 null,不为 null,将 r.next 指向 head,为 null,head 直接指向 rr.next = (head == null) ? r : head;// head 指针指向 rhead = r;queueLength++;if (r instanceof FinalReference) {sun.misc.VM.addFinalRefCount(1);}lock.notifyAll();return true;}
}
  • 判断 queue 为 null 或者 queue 已经被回收了,直接返回
  • 若 queue 不为 null,将 r (Reference) 的状态置为 Enqueued,表示已经被回收
  • 将 Reference 插入到 queue 的头部

Reference 和 ReferenceQueue 的源码分析到此为止


Reference 的子类

4种引用
我们都知道在Java中有4种引用,这四种引用从高到低分别为:

  1. StrongReference

这个引用在Java中没有相应的类与之对应,但是强引用比较普遍,例如:Object obj = new Object();这里的obj就是要给强引用,如果一个对象具有强引用,则垃圾回收器始终不会回收此对象。当内存不足时,JVM情愿抛出OOM异常使程序异常终止也不会靠回收强引用的对象来解决内存不足的问题。

  1. SoftReference

如果一个对象只有软引用,则在内存充足的情况下是不会回收此对象的,但是,在内部不足即将要抛出OOM异常时就会回收此对象来解决内存不足的问题。

public class TestSoftReference {private static ReferenceQueue<Object> rq = new ReferenceQueue<Object>();public static void main(String[] args){Object obj = new Object();SoftReference<Object> sf = new SoftReference(obj,rq);System.out.println(sf.get()!=null);System.gc();obj = null;System.out.println(sf.get()!=null);}}

运行结果均为:true。

这也就说明了当内存充足的时候一个对象只有软引用也不会被JVM回收。

  1. WeakReference

WeakReference 基本与SoftReference 类似,只是回收的策略不同。

只要 GC 发现一个对象只有弱引用,则就会回收此弱引用对象。但是由于GC所在的线程优先级比较低,不会立即发现所有弱引用对象并进行回收。只要GC对它所管辖的内存区域进行扫描时发现了弱引用对象就进行回收。

看一个例子:

public class TestWeakReference {private static ReferenceQueue<Object> rq = new ReferenceQueue<Object>();public static void main(String[] args) {Object obj = new Object();WeakReference<Object> wr = new WeakReference(obj,rq);System.out.println(wr.get()!=null);obj = null;System.gc();System.out.println(wr.get()!=null);//false,这是因为WeakReference被回收}}

运行结果为: true 、false

在指向 obj = null 语句之前,Object对象有两条引用路径,其中一条为obj强引用类型,另一条为wr弱引用类型。此时无论如何也不会进行垃圾回收。当执行了obj = null.Object 对象就只具有弱引用,并且我们进行了显示的垃圾回收。因此此具有弱引用的对象就被GC给回收了。

  1. PhantomReference

PhantomReference,即虚引用,虚引用并不会影响对象的生命周期。虚引用的作用为:跟踪垃圾回收器收集对象这一活动的情况。

当GC一旦发现了虚引用对象,则会将PhantomReference对象插入ReferenceQueue队列,而此时PhantomReference对象并没有被垃圾回收器回收,而是要等到ReferenceQueue被你真正的处理后才会被回收。

注意:PhantomReference必须要和ReferenceQueue联合使用,SoftReference和WeakReference可以选择和ReferenceQueue联合使用也可以不选择,这使他们的区别之一。

接下来看一个虚引用的例子。

public class TestPhantomReference {private static ReferenceQueue<Object> rq = new ReferenceQueue<Object>();public static void main(String[] args){Object obj = new Object();PhantomReference<Object> pr = new PhantomReference<Object>(obj, rq);System.out.println(pr.get());obj = null;System.gc();System.out.println(pr.get());Reference<Object> r = (Reference<Object>)rq.poll();if(r!=null){System.out.println("回收");}}}

运行结果:null null 回收

根据上面的例子有两点需要说明:

  • PhantomReference的get方法无论在上面情况下都是返回null。这个在PhantomReference源码中可以看到。

  • 在上面的代码中,如果obj被置为null,当GC发现虚引用,GC会将把 PhantomReference 对象pr加入到队列ReferenceQueue中,注意此时pr所指向的对象并没有被回收,在我们现实的调用了 rq.poll() 返回 Reference 对象之后当GC第二次发现虚引用,而此时 JVM 将虚引用pr插入到队列 rq 会插入失败,此时 GC 才会对虚引用对象进行回收。


总结

Reference 和引用队列 ReferenceQueue 联合使用时,如果 Reference 持有的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。


推荐阅读

一步步拆解 LeakCanary

Android 面试必备 - http 与 https 协议

Android 面试必备 - 计算机网络基本知识(TCP,UDP,Http,https)

Android 面试必备 - 线程

Android_interview github 地址

扫一扫,欢迎关注我的微信公众号 stormjun94, 目前专注于 Android 开发,主要分享 Android开发相关知识和技术人成长历程,包括个人总结,职场经验,面试经验等。

java 源码系列 - 带你读懂 Reference 和 ReferenceQueue相关推荐

  1. java源码系列:HashMap底层存储原理详解——4、技术本质-原理过程-算法-取模具体解决什么问题

    目录 简介 取模具体解决什么问题? 通过数组特性,推导ascii码计算出来的下标值,创建数组非常占用空间 取模,可保证下标,在HashMap默认创建下标之内 简介 上一篇文章,我们讲到 哈希算法.哈希 ...

  2. 16w行的nginx源码,如何才能读懂呢?全面分析nginx的机制

    16w行的nginx源码,如何才能读懂呢?全面分析nginx的机制 1. nginx的轮子组成 2. nginx的epoll实现机制 3. nginx的内存机制 视频讲解如下,点击观看: 16w行的n ...

  3. 华为OD机试(21-40)老题库解析Java源码系列连载ing

    华为OD机试算法题新老题库练习及源码 老题库 21.字符串序列判定 22.最长的指定瑕疵度的元音子串 23.处理器问题 24.单向链表中间节点 25.字符串重新排列.字符串重新排序 26.完美走位 2 ...

  4. 手撕Java源码系列之Map接口

    Map简介 #Map是以Key和Value进行存储的,这点就是与Collection做一个区别.Map的Key不可重复,但是value可以重复.而Collection最直观的感受就是其子接口Set,仅 ...

  5. JAVA源码系列-ArrayList

    前言 ArrayList是一个基于数组的数据结构,Java1.8版本加入了Lambda匿名内部类的新特性.而ArrayList实现了java.util.function的接口,进而为了支持Lambda ...

  6. java源码系列:HashMap底层存储原理详解——5、技术本质-原理过程-算法-取模会带来一个什么问题?什么是哈希冲突?为什么要用链表?

    目录 取模会带来一个什么问题? 演示什么是哈希冲突(哈希碰撞)? 为什么要用链表? 其他--布隆过滤器 取模会带来一个什么问题? 好,那同学们这样他能达到一个目的,但是呢,它也会带来的一个问题,那它会 ...

  7. 如何快速阅读java源码_如何速读?--如何快速阅读

    下面这些资料是我刚才在整理电脑硬盘的时候发现的,是以前自己刚刚开始关注自己阅读速度和质量的时候收集的资料.因为但是上面不方便,所以全是整理成了TXT文件保存下来,现在发到Blog里面,做个记录.速读要 ...

  8. 窥一斑而知全豹,几分钟带你读懂Java字节码,再也不怕了

    目录 1.如何看字节码 2.一个简单的例子 3.字节码结构 4.总结: 引言:都知道java的源文件最后会被编译成class文件,class文件的内容是字节码.为什么java要编译成字节码呐?我觉得最 ...

  9. JVM学习笔记(Ⅰ):Class类文件结构解析(带你读懂Java字节码,这一篇就够了)

    JVM学习笔记(Ⅰ):Class类文件结构解析,带你读懂Java字节码 前言:本文属于博主个人的学习笔记,博主也是小白.如果有不对的地方希望各位帮忙指出.本文主要还是我的学习总结,因为网上的一些知识分 ...

最新文章

  1. 2021年大数据Spark(四十五):Structured Streaming Sources 输入源
  2. 中科院微生物所王军课题组特别研究助理招聘
  3. MPB:宁大张德民组-对虾养殖系统微生物组样品的采集与制备
  4. 设置窗口大小后无法滚动_新款奥迪A6L更换变速器机电单元后无法完成油冷却阀基本设置...
  5. java hql多条件查询_Hibernate结合JPA编写通用泛型多条件查询
  6. Java知识整理——反射
  7. centos 卸载_CentOS「linux」学习笔记12:磁盘管理、分区挂载卸载操作
  8. 24个笔画顺序表_小学一年级语文26个汉语拼音字母要点+田字格儿歌,赶紧给孩子看...
  9. ubtunu打开firefox_Linux Ubuntu 终端命令行打开firefox报错
  10. 200道物理学难题——038蚱蜢跃树
  11. 同步手绘板——json
  12. u盘版linux initramfs,Linux_内核升级后 出现initramfs错误问题解决, 出现initramfs找不到/root /hos - phpStudy...
  13. Python中无法使用“~”获取Ubuntu系统的用户目录
  14. 30muduo_net库源码分析(六)
  15. 利用组策略进行软件分发
  16. cydia软件路径_ifile下载和安装及使用图文教程 强大的iPhone文件管理器
  17. win7计算机高级还原,win7怎么恢复出厂设置 win7恢复出厂设置方法【图文】
  18. 小鹤双拼鹤形简易入门-by小鹤双拼输入法QQ群用户-弧
  19. 计算机调剂到mba,这8种考生不能调剂!MBA/MPAcc等考研生注意
  20. Github标星25K+超火的Android实战项目,2022BTAJ面试真题详解

热门文章

  1. Python的皮肤的使用
  2. 京东商城主图、细节图、视频以及评论图分类下载方法
  3. 垃圾填埋场渗滤液厌氧处理过程中沼气的综合利用
  4. 常见的两种python编译器的安装
  5. 高防CDN怎么防攻击,能防什么攻击?
  6. 【iOS 开发】活动指示器控件 UIActivityIndicatorView
  7. java 短信验证码(创蓝接口)调用
  8. [转]谈谈工科学生如何学习数学
  9. efcore 实体配置_.NET 云原生架构师训练营(模块二 基础巩固 EF Core 基础与配置)--学习笔记...
  10. 汉源高科2个万兆光口16个千兆光口工业级以太网交换机机架式光纤自愈环网以太网交换机