Java 源码剖析——彻底搞懂 Reference 和 ReferenceQueue

本贴最后更新于 2676 天前,其中的信息可能已经时移世改

之前博主的一篇读书笔记——《深入理解 Java 虚拟机》系列之回收对象算法与四种引用类型博客中为大家介绍了 Java 中的四种引用类型,很多同学都希望能够对引用,还有不同类型引用的原理进行更深入的了解。因此博主查看了抽象父类 Reference 和负责注册引用对象的引用队列 ReferenceQueue 的源码,在此和大家一起分享,并做了一些分析,感兴趣的同学可以一起学习。

Reference 源码分析

首先我们先看一下 Reference 类的注释:

/** * Abstract base class for reference objects. This class defines the * operations common to all reference objects. Because reference objects are * implemented in close cooperation with the garbage collector, this class may * not be subclassed directly. 引用对象的抽象基类。此类定义了常用于所有引用对象的操作。因为引用对象是通过与垃圾回收器的密切合作来实现的,所以不能直接为此类创建子类。 */

该类提供了两个构造函数:

Reference(T referent) { this(referent, null); } Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; }

一个构造函数带需要注册到的引用队列,一个不带。带 queue 的意义在于我们可以吃从外部通过对 queue 的操作来了解到引用实例所指向的实际对象是否被回收了,同时我们也可以通过 queue 对引用实例进行一些额外的操作;但如果我们的引用实例在创建时没有指定一个引用队列,那我们要想知道实际对象是否被回收,就只能够不停地轮询引用实例的 get()方法是否为空了。值得注意的是虚引用 PhantomReference,由于它的 get()方法永远返回 null,因此它的构造函数必须指定一个引用队列。这两种查询实际对象是否被回收的方法都有应用,如 weakHashMap 中就选择去查询 queue 的数据,来判定是否有对象将被回收;而 ThreadLocalMap,则采用判断 get()是否为 null 来作处理。

接下来是它的主要成员:

private T referent; /* Treated specially by GC */

在这里我们首先明确一些名词,Reference 类也被称为引用类,它的实例 Reference Instance 就是引用实例,但是由于它是一个抽象类,它的实例只能是子类软(soft)引用,弱(weak)引用,虚(phantom)引用中的某个,至于引用实例所引用的对象我们称之为实际对象(也就是我们上面所写出的 referent)。

volatile ReferenceQueue<? super T> queue; /* 引用对象队列*/

queue 是当前引用实例所注册的引用队列,一旦实际对象的可达性发生适当的变化后,此引用实例将会被添加到 queue 中。

/* When active: NULL * pending: this * Enqueued: next reference in queue (or this if last) * Inactive: this */ @SuppressWarnings("rawtypes") Reference next;

next 用来表示当前引用实例的下一个需要被处理的引用实例,我们在注释中看到的四个状态,是引用实例的内部状态,不可以被外部查看或是直接修改:

  • Active:新创建的引用实例处于 Active 状态,但当 GC 检测到该实例引用的实际对象的可达性发生某些适当的改变(实际对象对于 GC roots 不可达)后,它的状态将会根据此实例是否注册在引用队列中而变成 Pending 或是 Inactive。
  • Pending:当引用实例被放置在 pending-Reference list 中时,它处于 Pending 状态。此时,该实例在等待一个叫 Reference-handler 的线程将此实例进行 enqueue 操作。如果某个引用实例没有注册在一个引用队列中,该实例将永远不会进入 Pending 状态。
  • Enqueued: 当引用实例被添加到它注册在的引用队列中时,该实例处于 Enqueued 状态。当某个引用实例被从引用队列中删除后,该实例将从 Enqueued 状态变为 Inactive 状态。如果某个引用实例没有注册在一个引用队列中,该实例将永远不会进入 Enqueued 状态。
  • Inactive:一旦某个引用实例处于 Inactive 状态,它的状态将不再会发生改变,同时说明该引用实例所指向的实际对象一定会被 GC 所回收。

事实上 Reference 类并没有显示地定义内部状态值,JVM 仅需要通过成员 queue 和 next 的值就可以判断当前引用实例处于哪个状态:

  • Active:queue 为创建引用实例时传入的 ReferenceQueue 的实例或是 ReferenceQueue.NULL;next 为 null
  • Pending:queue 为创建引用实例时传入的 ReferenceQueue 的实例;next 为 this
  • Enqueued:queue 为 ReferenceQueue.ENQUEUED;next 为队列中下一个需要被处理的实例或是 this 如果该实例为队列中的最后一个
  • Inactive:queue 为 ReferenceQueue.NULL;next 为 this
/* 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; /* When active: next element in a discovered reference list maintained by GC (or this if last) * pending: next element in the pending list (or null if last) * otherwise: NULL */ transient private Reference<T> discovered; /* used by VM */

看到注释的同学们有可能会有一些疑惑,明明 pending 是一个 Reference 类型的对象,为什么注释说它是一个 list 呢?其实是因为 GC 检测到某个引用实例指向的实际对象不可达后,会将该 pending 指向该引用实例,discovered 字段则是用来表示下一个需要被处理的实例,因此我们只要不断地在处理完当前 pending 之后,将 discovered 指向的实例赋予给 pending 即可。所以这个 static 字段 pending 其实就是一个链表。

private static class ReferenceHandler extends Thread { ...... public void run() { while (true) { tryHandlePending(true); } } }

ReferenceHandler 是一个优先级最高的线程,它执行的工作就是将 pending list 中的引用实例添加到引用队列中,并将 pending 指向下一个引用实例。

static boolean tryHandlePending(boolean waitForNotify) { ...... synchronized (lock) { if (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' chain pending = r.discovered; r.discovered = null; } } ...... ReferenceQueue<? super Object> q = r.queue; if (q != ReferenceQueue.NULL) q.enqueue(r); return true; }

Reference 对外提供的方法就比较简单了:

public T get() { return this.referent; }

get()方法就是简单的返回引用实例所引用的实际对象,如果该对象被回收了或者该引用实例被 clear 了则返回 null

public void clear() { this.referent = null; }

调用此方法不会导致此对象入队。此方法仅由 Java 代码调用;当垃圾收集器清除引用时,它直接执行,而不调用此方法。
clear 的方法本质上就是将 referent 置为 null,清除引用实例所引用的实际对象,这样通过 get()方法就不能再访问到实际对象了。

public boolean isEnqueued() { return (this.queue == ReferenceQueue.ENQUEUED); }

判断此引用实例是否已经被放入队列中是通过引用队列实例是否等于 ReferenceQueue.ENQUEUED 来得知的。

public boolean enqueue() { return this.queue.enqueue(this); }

enqueue()方法能够手动将引用实例加入到引用队列当中去。

ReferenceQueue 源码分析

同样我们先看一下 ReferenceQueue 的注释:

/** * Reference queues, to which registered reference objects are appended by the * garbage collector after the appropriate reachability changes are detected. * 引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中 */

ReferenceQueue 实现了队列的入队(enqueue)和出队(poll),其中的内部元素就是我们上文中提到的 Reference 对象。队列元素的存储结构是单链式存储,依靠每个 reference 对象的 next 域去找下一个元素。

主要成员有:

private volatile Reference extends T> head = null;

用来存储当前需要被处理的节点

static ReferenceQueue NULL = new Null<>(); static ReferenceQueue ENQUEUED = new Null<>();

static 变量 NUlL 和 ENQUEUED 分别用来表示没有提供默认引用队列的空队列和已经执行过 enqueue 操作的队列。

引用实例入队的逻辑很简单:

synchronized (lock) { // 检查reference是否已经执行过入队操作 ReferenceQueue<?> queue = r.queue; if ((queue == NULL) || (queue == ENQUEUED)) { return false; } //将引用实例的成员queue置为ENQUEUED r.queue = ENQUEUED; //若头节点为空,说明该引用实例为队列中的第一个元素,将它的next实例等于this //若头节点不为空,将它的next实例指向头节点指向的元素 r.next = (head == null) ? r : head; //头节点指向当前引用实例 head = r; //length+1 queueLength++; lock.notifyAll(); return true; }

简单来说,入队操作就是将每次需要入队的引用实例放在头节点的位置,并将它的 next 域指向旧的头节点元素。因此整个 ReferenceQueue 是一个后进先出的数据结构。

出队的逻辑为:

r指向头节点元素 Reference<? extends T> r = head; if (r != null) { //头节点指向null,如果队列中只有一个元素;否则指向r.next head = (r.next == r) ? null : r.next; //头节点元素的queue指向ReferenceQueue.NULL r.queue = NULL; //将r.next指向this r.next = r; //length-1 queueLength--; return r; }

总体来看,ReferenceQueue 的作用就是 JAVA GC 与 Reference 引用对象之间的中间层,我们可以在外部通过 ReferenceQueue 及时地根据所监听的对象的可达性状态变化而采取处理操作。

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3455 回帖 • 167 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3196 引用 • 8215 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • 导航

    各种网址链接、内容导航。

    43 引用 • 177 回帖
  • 游戏

    沉迷游戏伤身,强撸灰飞烟灭。

    181 引用 • 821 回帖
  • 京东

    京东是中国最大的自营式电商企业,2015 年第一季度在中国自营式 B2C 电商市场的占有率为 56.3%。2014 年 5 月,京东在美国纳斯达克证券交易所正式挂牌上市(股票代码:JD),是中国第一个成功赴美上市的大型综合型电商平台,与腾讯、百度等中国互联网巨头共同跻身全球前十大互联网公司排行榜。

    14 引用 • 102 回帖 • 316 关注
  • 百度

    百度(Nasdaq:BIDU)是全球最大的中文搜索引擎、最大的中文网站。2000 年 1 月由李彦宏创立于北京中关村,致力于向人们提供“简单,可依赖”的信息获取方式。“百度”二字源于中国宋朝词人辛弃疾的《青玉案·元夕》词句“众里寻他千百度”,象征着百度对中文信息检索技术的执著追求。

    63 引用 • 785 回帖 • 108 关注
  • Firefox

    Mozilla Firefox 中文俗称“火狐”(正式缩写为 Fx 或 fx,非正式缩写为 FF),是一个开源的网页浏览器,使用 Gecko 排版引擎,支持多种操作系统,如 Windows、OSX 及 Linux 等。

    7 引用 • 30 回帖 • 389 关注
  • webpack

    webpack 是一个用于前端开发的模块加载器和打包工具,它能把各种资源,例如 JS、CSS(less/sass)、图片等都作为模块来使用和处理。

    41 引用 • 130 回帖 • 250 关注
  • Hibernate

    Hibernate 是一个开放源代码的对象关系映射框架,它对 JDBC 进行了非常轻量级的对象封装,使得 Java 程序员可以随心所欲的使用对象编程思维来操纵数据库。

    39 引用 • 103 回帖 • 722 关注
  • Anytype
    3 引用 • 31 回帖 • 15 关注
  • Log4j

    Log4j 是 Apache 开源的一款使用广泛的 Java 日志组件。

    20 引用 • 18 回帖 • 31 关注
  • 安全

    安全永远都不是一个小问题。

    203 引用 • 818 回帖
  • 一些有用的避坑指南。

    69 引用 • 93 回帖
  • Latke

    Latke 是一款以 JSON 为主的 Java Web 框架。

    71 引用 • 535 回帖 • 826 关注
  • 阿里云

    阿里云是阿里巴巴集团旗下公司,是全球领先的云计算及人工智能科技公司。提供云服务器、云数据库、云安全等云计算服务,以及大数据、人工智能服务、精准定制基于场景的行业解决方案。

    84 引用 • 324 回帖
  • 宕机

    宕机,多指一些网站、游戏、网络应用等服务器一种区别于正常运行的状态,也叫“Down 机”、“当机”或“死机”。宕机状态不仅仅是指服务器“挂掉了”、“死机了”状态,也包括服务器假死、停用、关闭等一些原因而导致出现的不能够正常运行的状态。

    13 引用 • 82 回帖 • 79 关注
  • OpenCV
    15 引用 • 36 回帖 • 1 关注
  • 微软

    微软是一家美国跨国科技公司,也是世界 PC 软件开发的先导,由比尔·盖茨与保罗·艾伦创办于 1975 年,公司总部设立在华盛顿州的雷德蒙德(Redmond,邻近西雅图)。以研发、制造、授权和提供广泛的电脑软件服务业务为主。

    8 引用 • 44 回帖
  • Markdown

    Markdown 是一种轻量级标记语言,用户可使用纯文本编辑器来排版文档,最终通过 Markdown 引擎将文档转换为所需格式(比如 HTML、PDF 等)。

    170 引用 • 1529 回帖 • 1 关注
  • 支付宝

    支付宝是全球领先的独立第三方支付平台,致力于为广大用户提供安全快速的电子支付/网上支付/安全支付/手机支付体验,及转账收款/水电煤缴费/信用卡还款/AA 收款等生活服务应用。

    29 引用 • 347 回帖
  • 招聘

    哪里都缺人,哪里都不缺人。

    189 引用 • 1057 回帖
  • Caddy

    Caddy 是一款默认自动启用 HTTPS 的 HTTP/2 Web 服务器。

    12 引用 • 54 回帖 • 166 关注
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    139 引用 • 926 回帖
  • 知乎

    知乎是网络问答社区,连接各行各业的用户。用户分享着彼此的知识、经验和见解,为中文互联网源源不断地提供多种多样的信息。

    10 引用 • 66 回帖
  • Swagger

    Swagger 是一款非常流行的 API 开发工具,它遵循 OpenAPI Specification(这是一种通用的、和编程语言无关的 API 描述规范)。Swagger 贯穿整个 API 生命周期,如 API 的设计、编写文档、测试和部署。

    26 引用 • 35 回帖
  • Kubernetes

    Kubernetes 是 Google 开源的一个容器编排引擎,它支持自动化部署、大规模可伸缩、应用容器化管理。

    116 引用 • 54 回帖 • 1 关注
  • PWL

    组织简介

    用爱发电 (Programming With Love) 是一个以开源精神为核心的民间开源爱好者技术组织,“用爱发电”象征开源与贡献精神,加入组织,代表你将遵守组织的“个人开源爱好者”的各项条款。申请加入:用爱发电组织邀请帖
    用爱发电组织官网:https://programmingwithlove.stackoverflow.wiki/

    用爱发电组织的核心驱动力:

    • 遵守开源守则,体现开源&贡献精神:以分享为目的,拒绝非法牟利。
    • 自我保护:使用适当的 License 保护自己的原创作品。
    • 尊重他人:不以各种理由、各种漏洞进行未经允许的抄袭、散播、洩露;以礼相待,尊重所有对社区做出贡献的开发者;通过他人的分享习得知识,要留下足迹,表示感谢。
    • 热爱编程、热爱学习:加入组织,热爱编程是首当其要的。我们欢迎热爱讨论、分享、提问的朋友,也同样欢迎默默成就的朋友。
    • 倾听:正确并恳切对待、处理问题与建议,及时修复开源项目的 Bug ,及时与反馈者沟通。不抬杠、不无视、不辱骂。
    • 平视:不诋毁、轻视、嘲讽其他开发者,主动提出建议、施以帮助,以和谐为本。只要他人肯努力,你也可能会被昔日小看的人所超越,所以请保持谦虚。
    • 乐观且活跃:你的努力决定了你的高度。不要放弃,多年后回头俯瞰,才会发现自己已经成就往日所仰望的水平。积极地将项目开源,帮助他人学习、改进,自己也会获得相应的提升、成就与成就感。
    1 引用 • 487 回帖
  • OkHttp

    OkHttp 是一款 HTTP & HTTP/2 客户端库,专为 Android 和 Java 应用打造。

    16 引用 • 6 回帖 • 82 关注
  • Electron

    Electron 基于 Chromium 和 Node.js,让你可以使用 HTML、CSS 和 JavaScript 构建应用。它是一个由 GitHub 及众多贡献者组成的活跃社区共同维护的开源项目,兼容 Mac、Windows 和 Linux,它构建的应用可在这三个操作系统上面运行。

    15 引用 • 136 回帖 • 3 关注