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

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

之前博主的一篇读书笔记——《深入理解 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 引用 • 3453 回帖 • 201 关注
  • Java

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

    3186 引用 • 8212 回帖 • 1 关注

相关帖子

欢迎来到这里!

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

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

推荐标签 标签

  • CSDN

    CSDN (Chinese Software Developer Network) 创立于 1999 年,是中国的 IT 社区和服务平台,为中国的软件开发者和 IT 从业者提供知识传播、职业发展、软件开发等全生命周期服务,满足他们在职业发展中学习及共享知识和信息、建立职业发展社交圈、通过软件开发实现技术商业化等刚性需求。

    14 引用 • 155 回帖 • 1 关注
  • Telegram

    Telegram 是一个非盈利性、基于云端的即时消息服务。它提供了支持各大操作系统平台的开源的客户端,也提供了很多强大的 APIs 给开发者创建自己的客户端和机器人。

    5 引用 • 35 回帖
  • JetBrains

    JetBrains 是一家捷克的软件开发公司,该公司位于捷克的布拉格,并在俄国的圣彼得堡及美国麻州波士顿都设有办公室,该公司最为人所熟知的产品是 Java 编程语言开发撰写时所用的集成开发环境:IntelliJ IDEA

    18 引用 • 54 回帖
  • 宕机

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

    13 引用 • 82 回帖 • 52 关注
  • OpenResty

    OpenResty 是一个基于 NGINX 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

    17 引用 • 47 关注
  • CSS

    CSS(Cascading Style Sheet)“层叠样式表”是用于控制网页样式并允许将样式信息与网页内容分离的一种标记性语言。

    197 引用 • 547 回帖
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖 • 1 关注
  • C++

    C++ 是在 C 语言的基础上开发的一种通用编程语言,应用广泛。C++ 支持多种编程范式,面向对象编程、泛型编程和过程化编程。

    107 引用 • 153 回帖 • 3 关注
  • 小说

    小说是以刻画人物形象为中心,通过完整的故事情节和环境描写来反映社会生活的文学体裁。

    28 引用 • 108 回帖
  • etcd

    etcd 是一个分布式、高可用的 key-value 数据存储,专门用于在分布式系统中保存关键数据。

    5 引用 • 26 回帖 • 526 关注
  • 博客

    记录并分享人生的经历。

    273 引用 • 2388 回帖
  • 分享

    有什么新发现就分享给大家吧!

    247 引用 • 1792 回帖 • 7 关注
  • Log4j

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

    20 引用 • 18 回帖 • 32 关注
  • 友情链接

    确认过眼神后的灵魂连接,站在链在!

    24 引用 • 373 回帖 • 1 关注
  • Google

    Google(Google Inc.,NASDAQ:GOOG)是一家美国上市公司(公有股份公司),于 1998 年 9 月 7 日以私有股份公司的形式创立,设计并管理一个互联网搜索引擎。Google 公司的总部称作“Googleplex”,它位于加利福尼亚山景城。Google 目前被公认为是全球规模最大的搜索引擎,它提供了简单易用的免费服务。不作恶(Don't be evil)是谷歌公司的一项非正式的公司口号。

    49 引用 • 192 回帖
  • LeetCode

    LeetCode(力扣)是一个全球极客挚爱的高质量技术成长平台,想要学习和提升专业能力从这里开始,充足技术干货等你来啃,轻松拿下 Dream Offer!

    209 引用 • 72 回帖 • 1 关注
  • ngrok

    ngrok 是一个反向代理,通过在公共的端点和本地运行的 Web 服务器之间建立一个安全的通道。

    7 引用 • 63 回帖 • 622 关注
  • 微软

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

    8 引用 • 44 回帖
  • gRpc
    11 引用 • 9 回帖 • 61 关注
  • Ngui

    Ngui 是一个 GUI 的排版显示引擎和跨平台的 GUI 应用程序开发框架,基于
    Node.js / OpenGL。目标是在此基础上开发 GUI 应用程序可拥有开发 WEB 应用般简单与速度同时兼顾 Native 应用程序的性能与体验。

    7 引用 • 9 回帖 • 388 关注
  • 房星科技

    房星网,我们不和没有钱的程序员谈理想,我们要让程序员又有理想又有钱。我们有雄厚的房地产行业线下资源,遍布昆明全城的 100 家门店、四千地产经纪人是我们坚实的后盾。

    6 引用 • 141 回帖 • 584 关注
  • OpenShift

    红帽提供的 PaaS 云,支持多种编程语言,为开发人员提供了更为灵活的框架、存储选择。

    14 引用 • 20 回帖 • 624 关注
  • 知乎

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

    10 引用 • 66 回帖
  • Flume

    Flume 是一套分布式的、可靠的,可用于有效地收集、聚合和搬运大量日志数据的服务架构。

    9 引用 • 6 回帖 • 621 关注
  • 链书

    链书(Chainbook)是 B3log 开源社区提供的区块链纸质书交易平台,通过 B3T 实现共享激励与价值链。可将你的闲置书籍上架到链书,我们共同构建这个全新的交易平台,让闲置书籍继续发挥它的价值。

    链书社

    链书目前已经下线,也许以后还有计划重制上线。

    14 引用 • 257 回帖
  • Android

    Android 是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

    334 引用 • 323 回帖
  • Scala

    Scala 是一门多范式的编程语言,集成面向对象编程和函数式编程的各种特性。

    13 引用 • 11 回帖 • 124 关注