前言


 文章

一 概述


 简介

    PhantomReference(虚引用)类是Reference(引用)类的四大子类之一,只被虚引用持有的对象被称为虚可达(phantom reachable)对象。在Java中关于虚引用的定义是:虚引用等价于无引用,其无法对对象的生命周期产生任何影响,即对象的诞生、初始化、使用及回收完全与虚引用无关。这段描述与WeakReference(弱引用)类完全相同,以至于令人怀疑是不是复制黏贴的(反正我是复制黏贴的)…但实际上两者确实高度相似。

    虚引用类被设计专用于跟踪对象的GC状态,即判断对象是否已/会被GC回收。当GC发生时,如果发现某个该回收的对象存在虚引用,则会将该对象的虚引用加入到引用队列中,而当我们从引用队列中获取到了出队的虚引用,就可以判定其所指对象已/会被GC回收了(这种说法其实并不正确,但近似可以这样理解,具体会在下文讲述)。

    虚引用类有两个特点:一是必须与引用队列搭配使用,这与其本身的作用相关;二是其get()方法无法获取所指对象,即使其没有被GC回收。如此设计的目的是为了凸显其作用,虚引用类的设计者(Mark Reinhold:马克·莱因霍尔德)目的非常明显,他只希望虚引用类被用于其仅有的设计作用上(跟踪对象被垃圾回收的活动),除此以外没有任何使用它的理由。

    虚引用类可作为finalize()方法的替代方案。由于虚引用的存在,开发者能够知晓其所指对象是否已/会被GC回收,也因此可在所指对象被GC回收时执行自定义操作,这有点类似于方法finalize()的作用,因此虚引用类也被提倡作为finalize()方法的替代方案,毕竟finalize()方法本身存在着诸多缺陷…实际上弱引用类同样可作为finalize()方法的替代方案,但其自身特性决定了其可用于功能更加强大的场景。而虚引用类很专一…除了这事儿我啥都干不了,甚至还专门为此拓展了一个子类Cleaner(清洁工)类

 与弱引用的区别

    如果说在JDK9之前,虚引用还和弱引用还有着明显差别的话,那到了JDK9之后,基本上可以说两者已经完全相似了(该知识点会在下文详述)。两者都不会对所指对象的生命周期产生影响,并且都会在所指对象被GC回收后将引用加入引用队列中(如果弱引用注册了引用队列的话),因此弱引用类也完全可以达到与虚引用类相同的作用,那是否可以考虑将两者的其中之一删除呢?答案是不能。因为两者还是有一定区别的。

    虚引用类的get()方法永远返回null。虚引用类重写了get()方法,不论所指对象是否已/会被GC回收,该方法都会固定返回null。如此行为的目的是为了避免开发者通过虚引用影响所指对象的GC状态,即防止所指对象断开所有强引用后,开发者通过虚引用的get()方法获取到该所指对象并与GC Roots重新建立直接/间接的引用关系而使其复活。而弱引用类(以及其它引用类)则是可以做到这一点的,因为GC何时断开引用与其所指对象关联这件事情非常玄学,有时即使引用已经被加入引用队列,也依然可以通过get()方法获取到其所指对象,这也是我每次在描述“判断对象是否已/会被GC回收”时都会添加“会”字描述的原因。同时,由于无法获取所指对象,虚引用类也无法代替弱引用类在某些场景中发挥的作用,最典型的就是WeakHashMap(弱哈希映射)类,如果使用虚引用类来代替弱引用类,那该如何获取具体的键对象呢?

    虚引用类的入队时间与弱引用类不同。引用的入队时间与Finalization(终结)机制有关,即与所指对象的finalize()方法的调用有关。对于弱引用来说,其加入引用队列的时间在finalize()方法调用之前,即弱引用的处理优先级大于FinalReference(终引用),这在一定程度上会延长所指对象被GC回收前的停留时间,这就为其的复活提供了更大的可能。而虚引用的入队时间则在finalize()方法调用之后,即虚引用的处理优先级小于中引用,因此基本可以认为虚引用在入队时所指对象已经被GC回收了(而且实际上对虚引用讨论入队时间没有意义,因为你无法通过虚引用获取所指对象)。

二 使用


 创建

    虚引用类只有一个构造方法,该构造方法会同时指定所指对象及引用队列,这是由于虚引用在设计上被专用于跟踪所指对象的GC状态,因此其必须搭配引用队列才可使用。

  • public PhantomReference(T referent, ReferenceQueue<? super T> q) —— 创建指定所指对象及注册引用队列的虚引用。

 方法

    虚引用类没有定义新方法,其所有方法皆继承/实现自引用抽象类。

  • public T get() —— 获取 —— 获取当前虚引用的所指对象,该方法会固定返回null。
        虚引用类重写了get()方法,不论所指对象是否已/会被GC回收,该方法都会固定返回null。如此行为的目的是为了避免开发者通过虚引用影响所指对象的GC状态,即防止所指对象断开所有强引用后,开发者通过虚引用的get()方法获取到该所指对象与GC Roots重新建立引用关系而使其复活,具体源码如下:
/**
 * Returns this reference object's referent.  Because the referent of a
 * phantom reference is always inaccessible, this method always returns
 * <code>null</code>.
 *
 * @return <code>null</code>
 */
@Override
public T get() {
    // 永远返回null。
    return null;
}
  • public void clear() —— 清除 —— 清除当前虚引用的所指对象(即断开两者的引用关系),并不会将当前虚引用加入到注册引用队列中。该方法专为开发者提供,GC线程不会调用该方法断开当前虚引用与其所指对象的关联。

  • public boolean isEnqueued() —— 是否入队 —— 判断当前虚引用是否已加入注册引用队列,是则返回true;否则返回false。引用加入注册引用队列时会将自身注册的引用队列替换为“入队”引用队列,这是一个在引用队列类内部创建的全局静态引用队列,被作为引用加入注册引用队列的标志位来使用。因此判断当前引用是否加入注册引用队列无需遍历注册引用队列,直接判断注册引用队列是否是“入队”引用队列即可。

  • public boolean enqueue() —— 入队 —— 将当前虚引用加入注册引用队列中,成功返回true;否则返回false。该方法底层调用引用队列类的enqueue(Reference<? extends T> r) 方法实现。该方法专为开发者提供,“引用处理器”线程不会调用该方法将当前虚引用加入注册引用队列。

三 回收时机


    虚可达对象在何时被GC回收是虚引用的难点,且根据不同版本在机制上也有所区别。在JDK8及之前,虽然GC可能已经将虚可达/所指对象的虚引用加入引用队列中,但此时的虚可达/所指对象实际上并没有被GC回收,也就是说,虚引用会阻止虚可达/所指对象被GC回收。那虚可达/所指对象具体会在什么时候被GC回收呢?在源码中有这样一段注解。

    在JDK8及之前,虚可达/所指对象并不会因为其虚引用被加入到引用队列中而被GC回收。其具体的回收时机有二:一是虚引用调用了clear()方法,断开了与虚可达/所指对象的引用关联,此时的虚可达/所指对象变成了无引用(不可达)对象,则自然会被GC回收;二是虚引用被判断为可回收,则与之关联的虚可达/所指对象自然也会被GC回收。

    而从JDK9开始,GC会将虚可达/所指对象回收后再将虚引用加入引用队列中,即只要虚引用存在于引用队列中便意味着虚可达/所指对象已/会被GC回收…虽然开发者依然要通过出队才能知道其是否存在。从机制上看,显然是JDK9的机制符合标准流程,那为什么会有这么大的区别呢?其实也没有深层次的理由,早期由于涉及到专利的原因没有办法这么写,现在专利期过了自然也就更换了更优的写法。

06-12 01:33