使用Java 8中使用幻影参考的最佳方法,还是使用弱参考?

发布于 2025-01-23 19:35:38 字数 2779 浏览 1 评论 0原文

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

我的第一个想法是使用幻影参考,通常用作finalize()方法的改进。我将有一个幻影参考类,它将指向我的主要对象(我想检测到它是否在使用之前丢弃的对象)作为转介,类似的是:

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
        }
    }
}

但是,我使用的是Java 8,在Java 8中,幻影参考被征入参考队列时不会自动清除。 (我知道这是在Java 9中修复的,但是不幸的是,我必须使用Java8。)这意味着,一旦GC确定主要对象无法实现强烈的到达,并且出现幻影参考,它仍然无法收回内存在主要对象中,直到我在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
        }
    }
}

我正在考虑的另一个想法是使用弱参考而不是幻影参考。与Phantom的参考文献不同,当被征用时会清除弱参考文献,因此它不会阻止参考文献被收回。我了解,通常使用幻影参考来清理资源的原因,是因为幻影参考仅在最终确定并保证不再使用后才出现,而在最终确定之前已重新使用弱参考,因此无法释放资源,因此无法释放资源。但是,最终化器可能会复活对象。但是,在我的情况下,这并不是一个问题,因为我不是在“清理”任何资源,而只是提出一份报告,说我的主要对象在被使用之前被丢弃了,可以在对象仍在存储器中时完成。我的主要对象也没有finalize()方法,因此不必担心复活对象。那么,您认为弱参考对我的情况会更好吗?

I am implementing a feature that reports an error when instances of my Java class are discarded before being "used" (for simplicity, we can define being "used" as having a particular method called).

My first idea is to use phantom references, which is often used as an improvement on finalize() methods. I would have a phantom reference class that would point to my main object (the one I want to detect whether it is discarded before being used) as the referrent, something like this:

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
        }
    }
}

However, I am using Java 8, and in Java 8, phantom references are not automatically cleared when they are enqueued into the reference queue. (I know that this is fixed in Java 9, but unfortunately, I must use Java 8.) This means that, once the GC determines that the main object is not strongly reachable, and enqueues the phantom reference, it still cannot reclaim the memory of the main object, until I manually clear the phantom reference after I dequeue it in checkDiscarded(). I am concerned that, during the period of time between the GC enqueuing the phantom reference and me dequeueing it from the queue, the main object will remain in memory when it's unnecessary. My main object references many other objects which take a lot of memory, so I would not want it staying in memory for longer than without this feature.

To avoid this problem of the phantom reference preventing the main object from being reclaimed, I came up with the idea of using a dummy object as the referrent of the phantom reference instead of my main object. This dummy object will be referenced from my main object, so it will become unreachable at the same time as my main object. Since the dummy object will be small, I don't mind it not being reclaimed for longer period of time, as long as my main object will be reclaimed as soon as it's not reachable. Does this seem like a good idea, and is it really better than using the main object as the referrent?

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
        }
    }
}

Another idea I am considering is to use weak references instead of phantom references. Unlike phantom references in Java 8, weak references are cleared when they are enqueued, so it does not prevent the referrent from being reclaimed. I understand that the reason why phantom references are usually used for resource cleanup, is that phantom references are only enqueued after the referrent is finalized and guaranteed to not be used anymore, whereas weak references are enqueued before being finalized, and so resources cannot be freed yet, and also the finalizer might resurrect the object. However, that's not a concern in my case, as I am not "cleaning up" any resources, but just making a report that my main object was discarded before being used, which can be done while the object is still in memory. My main objects also do not have a finalize() method, so there is no concern of resurrecting the object. So do you think weak references would be a better match for my case?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

夏末的微笑 2025-01-30 19:35:38

当不涉及最终确定时,弱和幻影的参考确实是等效的。但是,一个常见的误解是假设对象仅在其自身类具有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&nbsp; 18已将finalize()方法标记为 dovenceed,以删除,因此将来此问题将在未来消失,而无需您采取任何措施。


尽管如此,您使用带有phantomReference的虚拟对象的其他方法仍会按预期工作,只有在虚拟对象且因此,外部对象也不再可以达到最终的幻象。由于附加的虚拟对象,缺点是(非常)稍高的内存消耗。

请注意,markused()方法可以将dummyObject设置为null to。


另一个可能的观点是,当您的功能旨在记录错误的类别时,通常不会发生这种情况时,当它发生时可能会暂时消耗更多内存 时,它都没关系。当调用Markused()时,幻象参考将被清除并留在垃圾收集而不会被征用的情况下,因此,在正确的使用情况下,内存的保留时间不会超过必要。

Weak and phantom references are indeed equivalent when no finalization is involved. However, a common misconception is to assume that an object is only subject to finalizer reachability and potential resurrection when its own class has a finalize() method.

To demonstrate the behavior, we may use

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");
}

This typically prints either,

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

or

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

as both references are truly treated the same in this case and there’s no preferred order when both are enqueued in the same garbage collection.

But when we replace the // ... line with

class Legacy {
    private Object finalizerReachable;

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

The output changes to something like

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

as Legacy’s finalizer is enough to make the the object finalizer reachable and open the possibility to resurrect the object during finalization.

This doesn’t have to stop you from using this approach. You may decide that there’s no such finalizer in your entire application or accept this scenario as known limitation, to only apply if someone intentionally adds such a finalizer. JDK 18 has marked the finalize() method as deprecated, for removal, so this issue will disappear in the future without requiring you to take any action.


Still, your other approach using a dummy object with a PhantomReference will work as intended, having the phantom reference only enqueued when the dummy object and hence, also the outer object, is not even finalizer reachable anymore. The drawback is the (very) slightly higher memory consumption due to the additional dummy object.

Mind that the markUsed() method may set dummyObject to null to.


Another possible point of view is that when your feature is intended to log a wrong usage of your class, which should normally not happen, it doesn’t matter when it might temporarily consume more memory when it happens. When markUsed() has been called, the phantom reference is cleared and left to garbage collection without getting enqueued, so in the case of a correct usage, the memory is not held longer than necessary.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文