为什么PhantomReference.get()方法总是返回null?

4
我了解不同类型的引用、可达性以及垃圾回收器的工作原理 - 我在一些个人项目中也使用了不同类型的引用。然而,有一个设计选择我不太理解:为什么PhantomReferenceget方法总是返回nullPhantomReference主要(可能仅仅)用于对象“死亡”后的清理操作。所以,对于我的问题,通常的答案是“确保在清理开始后,对象不能再次变得(强)可达” - 或者,正如PhantomReference的文档所述:

为了确保可回收对象保持可回收状态,幻影引用的引用对象不可被检索:幻影引用的get方法始终返回null

但就我理解,“确保可回收对象保持不变”并不意味着get()必须始终返回null:只要幻影引用没有被清除,它的引用对象就不可回收,也不会被加入队列,因此清理操作不会启动 - 这意味着get()可以“安全地”访问引用对象。只有在引用被清除后,它才变得可回收,所以get()从那时起无论如何都会返回null。也不存在竞争条件的可能性(例如:一个对象有两个幻影引用;一个被清除并加入队列,然后通过另一个的get方法重新访问对象),因为文档指出所有清除操作都是原子性的(我强调):

在那个时候,它将原子性地清除该对象的所有幻影引用以及该对象可达的其他幻影可达对象的所有幻影引用。


因为在评论中提出了这个问题:我提出这个问题并不是因为我有一个具体的问题,我认为这可能是解决方案。相反,我需要在我的硕士论文中描述各种“引用”类型,它们的相关性,并且我想正确地描述它们-包括设计选择背后的推理。
然而,我仍然可以想到一个明确不适用于使用“弱引用”的具体情况:
假设你正在编写一个为用户提供某种服务的库。让我们假设该库希望确保一次只存在一个此服务的实例-可以说它是一个“单例”-所以让我们将这个类称为“PublicSingletonService”。然而,让我们假设该服务需要许多资源才能存在-因此,一旦不再需要它,库就希望将其丢弃,并在需要时重新创建它。我认为这是一个合理的情况。
所以,该库对其单例持有一个WeakReference,并使用ReferenceQueue来检测单例是否不再需要并进行清理。当用户请求服务时,库会在WeakReference上调用get(),如果结果非null,则返回该结果,如果为null,则重新创建服务。
但是现在,使用该库的应用程序执行以下操作:
  1. 请求服务单例
  2. 创建一个对象(称为“持有者”),该对象引用服务单例,并具有一个finalizer,该finalizer将使对象(以及服务单例)再次可达。
  3. 清除所有对服务单例和持有者的剩余引用,使其仅通过库的弱引用弱可达
  4. 使用大量内存(或调用System.gc(),以便GC清除弱引用并启动持有者的finalization
  5. 以下三件事以任意顺序发生:
  • 持有者的finalizer "复活"了服务的单例
  • 库在单例之后进行清理
  • 应用程序请求另一个服务的单例 - 这将是新创建的。 现在,尽管库没有任何问题的finalizer,应用程序有两个单例实例!

库可以通过保留PhantomReference来部分防止这种情况,除了WeakReference之外,并且只有在PhantomReference被清除时才进行清理或创建第二个实例。然而,这有一个问题:如果WeakReference被清除,但是PhantomReference没有被清除,并且应用程序要求库提供服务的实例,库既无法访问旧实例也无法创建新实例。

如果PhantomReference.get()不总是返回null,这个问题就可以解决:只需让库使用PhantomReference而不是WeakReference,并使用PhantomReference.get()

这里是一个演示这个问题的示例程序:

package weirdjava;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.Semaphore;

import weirdjava.StronglyReachableAfterWeakReferenceCleared.Library.PublicSingletonService;

public class StronglyReachableAfterWeakReferenceCleared
{
    public static void main(String[] args) throws InterruptedException
    {
        Application.run();
    }

    public static class Application
    {
        public static void run() throws InterruptedException
        {
            // Application code initializes the library
            Library library = new Library();
            // "Library" creates the singleton of the public service and tries to detect once it is _definitely_ unreachable.
            PublicSingletonService singleton = library.getSingleton();

            // The application code then does weird stuff with the singleton (maliciously, bad programming, or convoluted interactions):
            // make holder
            ApplicationHolder holder = new ApplicationHolder(singleton);
            // clear the last remaining strong ref to the singleton
            singleton = null;
            // make sure the libraries' weakref is actually cleared - simulates the application using much memory.
            System.gc();

            // The "library" tries to detect whether the object was cleared, and clean up if it is.
            // In this example, we made sure that it is.
            library.doCleanupIfNecessary();

            // The application asks the library for the singleton again - because it has been cleared, the library recreates the singleton.
            PublicSingletonService secondSingleton = library.getSingleton();

            // Now, the application magically resurrects the first singleton
            PublicSingletonService resurrectedSingleton = holder.getReferent();
            if(resurrectedSingleton == null)
                throw new IllegalStateException("example failed");

            System.out.println("Resurrection successful! resurrected: " + resurrectedSingleton);

            // At this point, the weak references to the object have been cleared,
            // but the object is still strongly reachable -
            // and that without the libraries' class having any finalizer.
            System.out.println("Application now has two singletons: " + resurrectedSingleton + ", " + secondSingleton);
        }
        private static class ApplicationHolder
        {
            private final Semaphore     referrerFinalizationComplete;
            private FinalizableReferrer referrerAfterFinalization;

            public ApplicationHolder(PublicSingletonService referent)
            {
                referrerFinalizationComplete = new Semaphore(0);
                // explicitly don't keep a reference to this, to let it be able to be reclaimed
                new FinalizableReferrer(this, referent);
            }

            public PublicSingletonService getReferent() throws InterruptedException
            {
                referrerFinalizationComplete.acquire();
                return referrerAfterFinalization.getReferent();
            }

            private static class FinalizableReferrer
            {
                private final ApplicationHolder referrer;
                private final PublicSingletonService    referent;

                public FinalizableReferrer(ApplicationHolder referrer, PublicSingletonService referent)
                {
                    this.referrer = referrer;
                    this.referent = referent;
                }

                @Override
                protected void finalize()
                {
                    System.out.println("Finalizer making referent reachable again");
                    referrer.referrerAfterFinalization = this;
                    referrer.referrerFinalizationComplete.release();
                }

                public PublicSingletonService getReferent()
                {
                    return referent;
                }
            }
        }
    }

    public static class Library
    {
        // just for demonstration; in reality, this would have to be thread-safe and whatnot
        private int instanceCounter = 0;

        private final ReferenceQueue<PublicSingletonService>    refqueue;
        private WeakReference<PublicSingletonService>       publicSingletonObjectRef;

        public Library()
        {
            this.refqueue = new ReferenceQueue<>();
        }

        public void doCleanupIfNecessary() throws InterruptedException
        {
            // In practice, this would be poll(), not remove() - however, in this example,
            // even though we called System.gc() the enqueueing doesn't happen fast enough.
            Reference<? extends PublicSingletonService> poll = refqueue.remove();
            if(poll != null)
            {
                System.out.println("Reference to referent was cleared; weakRef.get() is " + publicSingletonObjectRef.get()); // is null
                // The library cleans up what it needs to clean up
                System.out.println("Library \"cleans up\"");
            } else
                // In practice, the library would do nothing here.
                throw new IllegalStateException("example failed");
        }

        public PublicSingletonService getSingleton()
        {
            if(publicSingletonObjectRef != null)
            {
                // This is the call to get() which would not be possible with a phantom reference
                PublicSingletonService previousInstance = publicSingletonObjectRef.get();
                if(previousInstance != null)
                    return previousInstance;
            }

            PublicSingletonService publicSingletonObjectInstance = new PublicSingletonService("Instance #" + instanceCounter ++);
            publicSingletonObjectRef = new WeakReference<>(publicSingletonObjectInstance, refqueue);
            return publicSingletonObjectInstance;
        }

        public static class PublicSingletonService
        {
            private final String value;

            public PublicSingletonService(String value)
            {
                this.value = value;
                System.out.println("Creating singleton object: " + value);
            }

            @Override
            public String toString()
            {
                return value;
            }
        }
    }
}

对于你的示例情况,(从语言层面上来说)解决方案不是搞虚引用。答案是摒弃 finalizers(终结器)。它们是一个错误,已经被弃用,并且 finalize 方法将在未来的 Java 版本中被移除。 - undefined
是的,那绝对是理想的解决方案 - 但在一个理想的世界中,不存在finalizers,我们根本不需要PhantomRefs,对吗?那么,它们将等同于WeakRefs(除了不支持get),或者我漏掉了什么? - undefined
所以,语言设计者引入PhantomRefs除了绕过finalizers之外,没有其他理由。或者这里的意图是,如果finalizer不能使对象可达,那么其他人也不能吗? - undefined
可能是为了实现更高效的收集策略,并且因为获取引用对象对于PhantomReference所设计的任务并没有用处。PhantomReference的设计目的是实现事后清理,而不是对引用对象本身进行操作。 - undefined
这如何使收集策略更高效?为什么在没有终结器的情况下不能使用WeakReferences进行事后清理?在有终结器的情况下,确实,WeakRefs不足以确保对象已经"死亡" - 但是我的例子表明,PhantomRefs上的get()将有一个合理的用途。 - undefined
对我来说,虚引用(PhantomRefs)的存在似乎是通过说“我们需要处理终结器的存在,所以弱引用(WeakRef)对于清理工作不足够”来证明的,但是将get()方法用于虚引用被认为是不合理的,因为有人说“我们不需要处理终结器的存在,因为它们已经被弃用了,所以只需使用弱引用”——这似乎是自相矛盾的。 - undefined
2个回答

3
你指的是Java 20 API。但在这种情况下,了解历史是有用的。
看一下2022年的Java 8的PhantomReference文档。
与软引用和弱引用不同,虚引用在被加入队列时不会被垃圾回收器自动清除。通过虚引用可访问的对象将保持可访问状态,直到所有这些引用被清除或它们自身变得不可访问。
不要问为什么保持一个对象的幽灵可达性曾经是一个目标。这个问题以前已经被问过,但从未得到令人满意的答案。这也是为什么这个规则在Java 9中被移除的部分原因,而且似乎在最新的Java 8更新中也是如此。
但是,当一个已入队的PhantomReference没有被清除时,显然需要防止通过get()方法进行检索,以防止可回收对象的复活。诚然,深度反射在过去仍然有效,这也是API的这一部分没有经过深思熟虑的另一个指标。
目前的情况是,当虚引用被入队时会被清除,因此在进行事后清理时,没有任何阻止对象被回收的因素。
所以,原来的问题已经解决了。允许get()返回引用对象不会影响垃圾回收的工作方式。在进行最终化时,可以检索对象,但这只是一个语义问题,不会影响虚引用的逻辑,因为虚引用只在对象在最终化完成后再次变得不可达时入队。
然而,没有理由改变合同。为了虚引用的目的,不需要获取对象。这就是弱引用的作用,而且由于最终化已被标记为“已弃用,将被移除”,与最终化相关的任何问题都将消失。

没错,虽然最终化仍然存在,但使用弱引用时,复活的情况确实存在。然而,如果有人努力破坏你的程序,无论如何都无法阻止他们。不要让这样的人对你的代码库做出贡献。Java的语言机制并不是真正的安全措施。它们使编写糟糕的代码变得更困难,但无法阻止人们故意破坏事物。

话虽如此,按照你描述的方式使用垃圾回收来释放非内存资源的整个想法是有缺陷的。关于对象何时或是否会被回收,没有任何保证,因此在你的情况下,这种清理功能不值得开发的努力。但在最坏的情况下,它可能会彻底崩溃


-1
如果 .get() 起作用,你可以简单地这样做:
someField = phantomRef.get();

突然间,正在被收集的对象变得无法再被收集。毕竟,你将它分配给了一个字段 - 它又变得可访问了。如果在你有一个字段引用了一个垃圾收集对象之后不久它就被收集了,那就是那些绝对绝对绝对不可能发生的红线,JVM特性设计中不能越过的一条线。
如果它没有被收集,也就是说那个动作“取消”了垃圾收集,你就会遇到两个问题:
无论是什么过程“扫描”以确定对象是否可收集,都需要重新扫描,因为它无法知道.get()的结果被用于重新激活对象。因此,任何时间任何东西调用.get()都意味着对象需要重新扫描。在这一点上...允许所有幻影引用再次执行吗?那么我们将陷入无尽的循环。
或者,.get()需要向JVM标记此线程以某种“慢速记账模式”运行,其中跟踪任何.get()调用只是为了检查您是否重新激活它们。此外,即使您不将其分配给任何字段,只要调用get的方法尚未完成,它仍然是一个活动对象。
这些都不是实际可行的方法。在.get()上返回null并指定这是幻影引用的处理方式可以解决所有问题。

1
这些文档明确指出:PhantomRefs可以引用事物,即使它们已被标记为“可回收”。JVM没有系统来移除该标记(或者说,它不想这样做,因为那样会使GC系统变得复杂得多),所以它必须保持可回收状态。让你获取对象引用将意味着它必须变成不可回收的。这是不可能的。而且也没有必要为一些奇特的用例构建它,因为这些用例可以简单地使用WeakRef。 - undefined
SO不是一个适合询问“为什么会这样做”的地方,你需要向设计师提问并搜索相关的邮件列表。然而,根据我从那些邮件列表中记得的内容,这就是原因。我非常非常确定SO不是一个讨论所做选择的智慧程度的地方。这方面有由OpenJDK项目运营的邮件列表。 - undefined
答案已经提到了原因:因为这将要求GC系统支持一个被认为是“可回收”的对象后来仍然可以被引用(因此,不应再被回收)。很明显,这会使事情变得非常复杂。我不知道如何进一步解释,除非用大写字母写出来,我猜。 - undefined
我不认为你回答的那部分是正确的。java.lang.ref的文档中明确指出:“最后,当一个对象在以上任何方式中都不可达时,它将变得不可达,因此可以被回收”,其中“以上任何方式”包括虚引用。因此,在虚引用可达的情况下,对象是不可回收的,这使得你关于使可回收对象再次可达的观点无效。如果我在这个论证中有错误,请解释一下错误出在哪里。 - undefined
1
垃圾收集器不会将对象标记为“可回收”。相反,它将对象标记为“仍然可访问”。因此,当遇到特殊引用,如弱引用、软引用或虚引用时,它必须决定是清除它们(如文档所述,以原子方式)还是将它们视为可访问并遍历它们。即使在Java 9之前的行为中,幽灵引用也不会被清除。尽管get()方法总是返回null,垃圾收集器仍必须将未清除的引用视为可访问并遍历它们,从而降低性能但保持不可破坏。 - undefined
显示剩余11条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接