在 Java 8 中使用虚引用的最佳方式,还是改用弱引用?

Optimal way to use phantom references in Java 8, or use weak references instead?

我正在实现一项功能,当我的 Java class 的实例在被“使用”之前被丢弃时会报告错误(为简单起见,我们可以将被“使用”定义为具有特定的调用的方法)。

我的第一个想法是使用虚引用,它经常被用作对finalize()方法的改进。我会有一个幻影引用 class 指向我的主要对象(我想检测它是否在使用前被丢弃的对象)作为引用对象,如下所示:

class MainObject {
    static class MyPhantom extends PhantomReference<MainObject> {
        static Set<MyPhantom> phantomSet = new HashSet<>();
        MyPhantom(MainObject obj, ReferenceQueue<MainObject> queue) {
            super(obj, queue);
            phantomSet.add(this);
        }
        void clear() {
            super.clear();
            phantomSet.remove(this);
        }
    }
    MyPhantom myPhantom = new MyPhantom(this, referenceQueue);
    static ReferenceQueue<MainObject> referenceQueue = new ReferenceQueue<>();
    void markUsed() {
        myPhantom.clear();
        myPhantom = null;
    }
    static void checkDiscarded() { // run periodically
        while ((aPhantom = (MyPhantom) referenceQueue.poll()) != null) {
            aPhantom.clear();
            // do stuff with aPhantom
        }
    }
}

不过,我使用的是Java8,在Java8中,幻象引用在进入引用队列时不会自动清除。 (我知道这个在Java 9中是固定的,但不幸的是,我必须使用Java 8。)这意味着,一旦GC确定主对象不是强可达的,并且将phantom入队引用,它仍然无法回收主对象的内存,直到我在 checkDiscarded() 中将其出列后手动清除幻影引用。我担心的是,在 GC 将幻影引用排入队列和我将其从队列中取出之间的时间段内,主对象将在不需要时保留在内存中。我的主要对象引用了许多其他占用大量内存的对象,因此我不希望它在内存中停留的时间比没有此功能时长。

为了避免幻影引用阻止主对象被回收的问题,我想到了使用虚拟对象作为幻影引用的引用而不是我的主对象的想法。这个虚拟对象将从我的主要对象中引用,因此它将与我的主要对象同时变得不可访问。由于虚拟对象很小,我不介意它长时间不被回收,只要我的主要对象一旦无法访问就会被回收。这看起来是个好主意吗,它真的比使用主对象作为引用更好吗?

class MainObject {
    static class MyPhantom extends PhantomReference<Object> {
        static Set<MyPhantom> phantomSet = new HashSet<>();
        MyPhantom(Object obj, ReferenceQueue<Object> queue) {
            super(obj, queue);
            phantomSet.add(this);
        }
        void clear() {
            super.clear();
            phantomSet.remove(this);
        }
    }
    Object dummyObject = new Object();
    MyPhantom myPhantom = new MyPhantom(dummyObject, referenceQueue);
    static ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
    void markUsed() {
        myPhantom.clear();
        myPhantom = null;
    }
    static void checkDiscarded() { // run periodically
        while ((aPhantom = (MyPhantom) referenceQueue.poll()) != null) {
            aPhantom.clear();
            // do stuff with aPhantom
        }
    }
}

我正在考虑的另一个想法是使用弱引用而不是虚引用。与 Java8 中的虚引用不同,弱引用在入队时会被清除,因此不会阻止引用者被回收。我理解幻影引用通常用于资源清理的原因是幻影引用仅在引用完成后才入队并保证不再使用,而弱引用在完成前入队,因此无法释放资源然而,终结器也可能使对象复活。但是,在我的情况下,这不是问题,因为我不是在“清理”任何资源,而只是报告我的主要对象在使用前被丢弃,这可以在对象仍在内存中时完成。我的主要对象也没有 finalize() 方法,所以不关心复活对象。那么您认为弱引用更适合我的情况吗?

当不涉及终结时,弱引用和虚引用确实是等价的。然而,一个常见的误解是假设一个对象仅在其自身 class 具有 finalize() 方法时才受终结器可达性和潜在复活的影响。

为了演示该行为,我们可以使用

Object o = new Object();
ReferenceQueue<Object> q = new ReferenceQueue<>();
Reference<?> weak = new WeakReference<>(o, q), phantom = new PhantomReference<>(o, q), r;
// ...
o = null;
for(int cycles = 0, got = 0; got < 2; ) {
    while((r = q.remove(100)) == null) {
        System.gc();
        cycles++;
    }
    got++;
    System.out.println(
        (r == weak? "weak": r == phantom? "phantom": "magic unicorn")
      + " ref queued after " + cycles + " cycles");
}

这通常会打印,

phantom ref queued after 1 cycles
weak ref queued after 1 cycles

weak ref queued after 1 cycles
phantom ref queued after 1 cycles

因为在这种情况下两个引用实际上是相同的,并且当它们在同一个垃圾收集中排队时没有首选顺序。

但是当我们将 // ... 行替换为

class Legacy {
    private Object finalizerReachable;

    Legacy(Object o) {
        finalizerReachable = o;
    }
    @Override
    protected void finalize() throws Throwable {
        System.out.println("Legacy.finalize()");
    }
}
new Legacy(o);

输出变为

Legacy.finalize()
weak ref queued after 1 cycles
phantom ref queued after 2 cycles

因为 Legacy 的终结器足以使对象终结器可达,并开启了在终结期间复活对象的可能性。

这并不一定会阻止您使用这种方法。您可以决定在您的整个应用程序中没有这样的终结器,或者接受这种情况作为已知限制,仅在有人有意添加这样的终结器时才适用。 JDK 18 已将 finalize() 方法标记为 已弃用,以便删除 ,因此此问题将在未来消失,无需您采取任何操作。


不过,您使用带有 PhantomReference 的虚拟对象的其他方法将按预期工作,只有当虚拟对象以及外部对象甚至不再可访问终结器时,幻像引用才会排队.缺点是由于额外的虚拟对象,内存消耗(非常)略高。

请注意,markUsed() 方法可能会将 dummyObject 设置为 null


另一种可能的观点是,当您的功能旨在记录您的 class 的错误使用时,这通常不应该发生,它何时可能暂时消耗更多内存并不重要 当它发生时。当 markUsed() 被调用时,幻象引用被清除并留给垃圾收集而不排队,因此在正确使用的情况下,内存不会超过必要的时间。