前言
文章
- 相关系列:《Java ~ Reference【目录】》(持续更新)
- 相关系列:《Java ~ Reference ~ ReferenceQueue【源码】》(学习过程/多有漏误/仅作参考/不再更新)
- 相关系列:《Java ~ Reference ~ ReferenceQueue【总结】》(学习总结/最新最准/持续更新)
- 相关系列:《Java ~ Reference ~ ReferenceQueue【问题】》(学习解答/持续更新)
- 涉及内容:《Java ~ Reference【总结】》
- 涉及内容:《Java ~ Reference ~ FinalReference【总结】》
一 概述
简介
ReferenceQueue(引用队列)类是Reference(引用)框架中专门设计用来与Reference(引用)抽象类配合使用的队列,采用链表的方式以实现。其作用是追踪引用的所指对象的GC状态,即判断所指对象是否已/会被GC回收。如果一个引用注册了引用队列,并且其所指对象被GC判定为可回收,则该引用会被加入到注册引用队列中(实际上这里只是简单叙述,将引用加入注册引用队列中其实是有相关运行流程的,这个运行流程被称为Reference(引用)机制,该知识点会在讲解引用抽象类的文章中详述)。这就意味着引用队列中的引用的所指对象必然已/会被GC回收,因此加入注册引用队列的引用可作为其所指对象已被GC回收的判断依据,开发者可以通过从注册引用队列中获取引用的方式来判断其对应的所指对象是否已/会被GC回收,并以此为契机执行某些自定义操作,例如回收堆外内存等。
引用队列类的是无界队列,即容量理论上只受限于内容大小。
引用队列类的本质是堆栈。虽然命名为队列,但引用队列的插入/移除操作都会在头部发生,因此引用队列类的本质是堆栈而非队列,这也就意味着引用的移除是非公平的,即后插入的引用反而会被先移除。
引用队列类是线程安全的。引用队列类被用于多线程环境中,会被一条“引用处理器”线程及未知数量的用户线程共同访问,因此为了避免数据安全问题,其必须保证线程安全。引用队列类采用“单锁”线程安全机制,通过synchronized关键字来负责加锁,锁对象是其内部自实现的静态Null类对象…感觉完全没必要啊…用this不就可以了…
引用队列类并不是Collection(集)框架的成员。虽说名为队列,但引用队列类并没有实现/继承Queue(队列)接口或集框架范围内的其它接口/抽象类/类,因此引用队列类并不是集框架的成员,是完全由引用框架自实现的。
结构
二 使用
创建
- public ReferenceQueue() —— 创建引用队列。
方法
-
public Reference<? extends T> poll() —— 轮询 —— 从当前引用队列的头部移除并获取引用。该方法是移除方法中“特殊值”形式的实现,当引用队列中存在引用时移除并返回头引用;否则返回null。
-
public Reference<? extends T> remove() throws InterruptedException —— 移除 —— 从当前引用队列的头部移除并获取引用。该方法是移除方法中“阻塞”形式的实现,当引用队列中存在引用时移除并返回头引用;否则等待至存在引用。该方法直接调用“超时”形式的移除方法实现,其传入的超时参数为0,表示无限等待。
-
public Reference<? extends T> remove(long timeout) throws IllegalArgumentException, InterruptedException —— 移除 —— 从当前引用队列的头部移除并获取引用。该方法是移除方法中“超时”形式的实现,当引用队列存在元素时移除并返回头引用;否则在指定等待时间内等待至存在引用,超出指定等待时间则返回null。当超时参数传0时表示无限等待。
上述列举了引用队列类关于移除的所有方法,如果熟悉Queue(队列)/BlockingQueue(阻塞队列)接口定义的话会发现引用队列类在方法命名上与标准还是有所差异的(例如“阻塞”形式的实现在标准中被命名为take)。具体原因上文也已经提及,是因为引用队列类完全由引用架自实现,不是集框架成员的缘故。
虽说已经列举了所有的移除方法,但我们似乎还遗漏了些什么…是的,我们没有列举插入方法。是因为没有吗?当然不可能,引用队列类是存在插入方法的,只是该方法并非公共方法,用户线程无法直接访问,理论上只有引用机制的“引用处理器”线程才会通过同包下的引用抽象类来调用该方法。具体如下:
- boolean enqueue(Reference<? extends T> r) —— 入队 —— 从当前引用队列的头部插入指定引用,成功返回true;否则返回false。基于该方法“头插法”的实现方式可知引用队列类不是标准的FIFO实现,其本质是堆栈。
模板
/**
* 主方法
*
* @param args 参数集
*/
public static void main(String[] args) {
// 创建引用队列。
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
// 以该引用队列为注册引用队列创建若引用及其所指对象。
Object o1 = new Object();
Object o2 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);
PhantomReference<Object> phantomReference = new PhantomReference<>(o2, referenceQueue);
// 断开程序对所指对象的强引用。
o1 = null;
o2 = null;
// 主线程短暂等待1秒,确保JVM有充足的时间将引用加入引用队列中。
LockSupport.parkNanos(1000000000);
// 从引用队列中取出引用,从引用队列中取出的引用意味着其所指对象已/会被GC回收。
Reference<?> reference;
while ((reference = referenceQueue.poll()) != null) {
System.out.println("引用【" + reference + "】的所指对象已/会被GC回收。");
}
}
三 实现
“空”引用队列与“入队”引用队列
“空”引用队列与“入队”引用队列是引用队列类中创建的两个全局静态引用队列,是引用队列类的内部静态子类Null(空)类的对象,我们可以将之视为两个全局引用队列常量,虽然其并没有被修饰final关键字。空类重写了引用队列类的enqueue(Reference<? extends T> r)方法,令之固定返回false,意味着空类对象虽然是引用队列,但却永远都不会有引用入队。相关源码如下:
private static class Null<S> extends ReferenceQueue<S> {
@Override
boolean enqueue(Reference<? extends S> r) {
// 入队操作直接返回false,意味着引用永远都不可能成功入队。
return false;
}
}
static ReferenceQueue<Object> NULL = new Null<>();
static ReferenceQueue<Object> ENQUEUED = new Null<>();
“空”引用队列与“入队”引用队列分别被作为展示引用不同状态的标记值使用。引用存在状态的概念,用于表示其在引用机制中标的不同阶段,具体会在引用抽象类相关文章中详述。在这里我们只需要知道的是:“空”引用队列被作为引用未注册引用队列及引用已从注册引用队列出队的标记值。即当引用被创建时如果没有注册引用队列,则会将“空”引用队列作为默认注册引用队列(已知“空”引用队列是无法入队的)。以及如果引用已从注册引用队列中出队,则其注册引用队列也会被赋值为“空”引用队列。而对于如何区分引用是未注册引用队列还是已从注册引用中出队,则需要搭配引用中的其它条件综合判断,该知识点会在下文及引用抽象类相关文章中详述;而“入队”引用队列的作用则更加直观,被作为引用已加入注册引用队列的标志值,这一点从命名上就可以看出来…当引用于注册引用队列入队后,引用的注册引用队列会被赋值为“入队”引用队列。
自引用
引用队列会将尾引用/节点设置为自引用,即将尾引用/节点自身作为自身在引用队列中的后继引用/节点。尾节点在链表中的后继引用通常都为null,null本身既可以作为标记值也有助于GC。但有时如果基于流程的原因如果null已被作为其它情况的标记值,则自引用也是一种不错的替代方案。自引用不但可以作为标记值,并且在辅助GC中也有着不错的表现(虽然肯定是比不上null的)。
引用队列之所以会将尾引用/节点设置为自引用与引用的状态有关。引用的后继引用与注册引用队列一样,都是状态的综合判断条件之一,而“空”引用队列与后继引用为null便是引用未注册引用队列的状态值。因此为了与其它状态值进行区分,引用在引用队列中的后继引用不可为null。
终引用计数
当引用队列发现入队/出队的引用为FinalReference(终引用)时,会对之进行计数,即对整个JVM(而不是某个引用队列)中的终引用总数进行递增/减。该操作的具体作用未知。