【问题标题】:Phantom Referenced Objects幻像引用的对象
【发布时间】:2010-09-09 23:45:27
【问题描述】:

幻影引用用于事后分析。 Java 规范声明 幻像引用对象 在幻像引用本身被清除之前不会被释放。

我的问题是:这个特性(对象未释放)有什么用途?

(我想出的唯一想法是允许本地代码对对象进行事后清理,但这并没有太大的说服力)。

【问题讨论】:

    标签: java finalize phantom-reference


    【解决方案1】:

    编辑,因为我首先误解了这个问题:

    引用自这里http://www.memorymanagement.org/glossary/p.html

    Java 规范说 幻影参考未清除时 引用对象已入队,但 实际上,没有办法 语言来判断这是否已经 完成与否。在一些实现中, JNI 弱全局引用更弱 比幻影参考,并提供一个 访问幻象可达的方法 对象。

    但我没有找到其他相同的参考资料。

    【讨论】:

    • 我的问题不是关于 PhantomReferences 的有用性,而是关于为什么在清除 phantomreference 之前不释放对象。
    • 啊,好吧,我把 PhantomReferences 弄错了,比如this feature
    • 是的,我知道,但是您是否熟悉一些可以利用这一点的用例?
    【解决方案2】:

    我能想到的唯一一个可以防止释放的好的用例是,某种 JNI 实现的异步数据源正在写入被引用的对象,并且必须被告知停止写入 - 停止写入对象 - 在内存被回收之前。如果允许预先释放,一个简单的 forgot-to-dispose() 错误可能会导致内存损坏。

    这是过去会使用 finalize() 的情况之一,并且可能引发了它的一些怪癖。

    【讨论】:

      【解决方案3】:

      我认为这个想法是让其他对象在原始对象之外进行额外的清理。例如,如果无法扩展原始对象以实现某些终结性内容,则可以使用幻像引用。

      更大的问题是 JVM 不保证一个对象会被最终确定,而且我假设通过扩展不能保证幻像引用在最终确定后做他们的事情。

      【讨论】:

      • 再次,我的问题是,为什么对象在完成后会在内存中?您无法从 Java 代码访问它们,所以为什么要等到 PhantomReference 本身被清除?
      • @ShimiBandiel 考虑一个引用其他对象 A 和 B 的对象 O。对象 O 可能已完成,此时您可能希望在 A 和 B 完成之前运行一些清理。通过将其保持在幻像状态,JVM 允许您在清理 O 期间到达 A、B。
      【解决方案4】:
      幻影引用可用于执行垃圾回收前的操作,例如释放资源。相反,人们通常为此使用 finalize() 方法,这不是一个好主意。终结器对垃圾收集器的性能有可怕的影响,如果您不小心,可能会破坏应用程序的数据完整性,因为“终结器”是在随机线程中随机调用的。

      在幻影引用的构造函数中,您指定一个 ReferenceQueue,一旦被引用的对象变为“幻影可达”,幻影引用就会在其中排队。幻影可达意味着除了通过幻影引用之外无法访问。最初令人困惑的是,尽管幻像引用继续将被引用对象保存在私有字段中(与软引用或弱引用不同),但它的 getReference() 方法始终返回 null。这样您就无法再次使对象强可达。

      有时,您可以轮询 ReferenceQueue 并检查是否有任何新的 PhantomReferences 其引用的对象已变为幻影可访问。为了能够做任何有用的事情,例如可以从 java.lang.ref.PhantomReference 派生一个类,该类引用应该在垃圾收集之前释放的资源。仅当幻像引用本身变得无法访问时,才会对引用的对象进行垃圾回收。

      http://www.javalobby.org/java/forums/m91822870.html#91822413

      【讨论】:

      • 但是为什么调用 finalize() 后它没有被释放呢?为什么要等待 SoftReference 被清理?这可以统计什么用例?
      【解决方案5】:

      对于没有生命周期管理机制但您正在实施需要显式生命周期管理的 API 来说,这是一个完美的解决方案。

      特别是任何过去只使用内存中的对象的 API,但是您已经使用套接字连接或文件连接重新实现到其他更大的后备存储,可以使用 PhantomReference 来“关闭”和清理连接信息在对象被 GC 之前,连接从未关闭,因为没有您可以使用的生命周期管理 API 接口。

      考虑将一个简单的地图地图移动到数据库中。当地图引用被丢弃时,没有明确的“关闭”操作。然而,如果您已经实现了通过缓存写入,您希望能够完成任何写入并关闭与“数据库”的套接字连接。

      下面是我用于这类东西的一个类。请注意,对 PhantomReferences 的引用必须是非本地引用才能正常工作。否则,jit 将导致它们在您退出代码块之前过早排队。

      导入 java.lang.ref.PhantomReference; 导入 java.lang.ref.Reference; 导入 java.lang.ref.ReferenceQueue; 导入 java.util.ArrayList; 导入 java.util.List; 导入 java.util.concurrent.ConcurrentHashMap; 导入 java.util.concurrent.atomic.AtomicInteger; 导入 java.util.logging.Level; 导入 java.util.logging.Logger; /** * 该类提供了一种跟踪一种类型的参考丢失的方法 * 对象以允许使用辅助引用来执行一些清理 * 活动。最常见的用法是用于一个对象,它可能 * 包含或引用另一个需要执行一些清理的对象 * 当不再引用引用者时。 *

      * 一个例子可能是 Holder 类型的对象,它引用或使用一个 * 套接字连接。当引用丢失时,套接字应该是 * 关闭。因此,可以创建一个实例,如下所示 *

           * ReferenceTracker trker = ReferenceTracker() {
           * public void 已发布(Socket s){
           *            尝试 {
           * s.close();
           * } 捕捉(异常前){
           * log.log(Level.SEVERE, ex.toString(), ex);
           * }
           * }
           * };
           * 
      * 在某个地方,可能会有如下调用。 * * 接口持有人{ * public T get(); * } * 类 SocketHolder 实现 Holder { * 套接字; * 公共 SocketHolder( 套接字 sock ) { * s = 袜子; * } * 公共套接字 get() { * 返回 s; * } * } * * 这定义了持有者接口的实现,它持有 * 对 Socket 对象的引用。使用trker * 上面的对象可能包括使用创建方法的方法 * 对象和注册引用如下所示。 * * public SocketHolder connect(String host, int port) 抛出 IOException { * 套接字 s = 新套接字(主机,端口); * SocketHolder h = 新的 SocketHolder(s); * trker.trackReference(h, s); * 返回 h; * } * * 希望使用套接字连接并传递它的软件 * 在所有情况下,使用 SocketHolder.get() 来引用 Socket 实例。 * 然后,当所有 SocketHolder 引用都被删除时,套接字将 * 由所示的released(java.net.Socket) 方法关闭 * 更多。 *

      * {@link ReferenceTracker} 类对第一个参数使用 {@link PhantomReference} 作为 * 持有对第二个参数的引用的映射的键。因此,当 * key instance被释放,key引用入队,可以从中移除 * 队列,用于从映射中删除值,然后传递给 * 释放()。 */ 公共抽象类 ReferenceTracker { /** * 正在从引用队列 refqueue 中删除条目的线程实例。 */ 私有易失性 RefQueuePoll 轮询; /** * 用于此实例的 Logger 实例。它将包含名称作为后缀 * 如果使用了该构造函数。 */ private static final Logger log = Logger.getLogger(ReferenceTracker.class.getName()); /** * 名称表明这是用于记录和其他分离的实例 * 需要实例。 */ 私有最终字符串其中; /** * 使用传递的名称创建一个新的 ReferenceTracker 实例以区分 * logging 和 toString() 实现中的实例。 * @param which 该实例的名称用于区分日志记录等中的多个实例。 */ 公共参考跟踪器(字符串){ 这.哪个=哪个; } /** * 创建一个没有限定名称的 ReferenceTracker 新实例。 */ 公共参考跟踪器(){ this.which = null; } /** * 提供对该实例名称的访问。 * @return 此实例的名称。 */ @Override 公共字符串 toString() { 如果(其中==空){ return super.toString()+": ReferenceTracker"; } return super.toString()+": ReferenceTracker["+which+"]"; } /** * 子类必须实现此方法。当所有对 * 关联的持有者对象被删除。 * @param val 作为第二个参数传递给相应调用 {@link #trackReference(Object, Object) trackReference(T,K)} 的值 */ 公共抽象无效释放(K val); /** 对持有者对象的引用的引用队列 */ 私有最终 ReferenceQueuerefqueue = new ReferenceQueue(); /** * 已创建并销毁为条目的线程总数 * 被跟踪。当跟踪引用为零时,没有队列运行。 */ 私有最终 AtomicInteger tcnt = new AtomicInteger(); 私有 volatile 布尔运行; /** * 一个线程实现,它轮询 {@link #refqueue} 以随后调用 {@link released(K)} * 作为对 T 对象的引用被删除。 */ 私有类 RefQueuePoll 扩展线程 { /** * 与此实例关联的线程号。可能有两种情况 * 此类存在于易失性系统中。如果是这种情况,该值将 * 在某些日志中可见以区分活动日志。 */ 私人最终 int mycnt; /** * 创建这个类的一个实例。 */ 公共 RefQueuePoll() { 设置守护进程(真); setName(getClass().getName()+": ReferenceTracker("+which+")"); mycnt = tcnt.incrementAndGet(); } /** * 此方法提供执行refqueue.remove()的所有活动 * 调用然后调用released(K) 让应用程序释放 * 所需资源。 */ 公共@Override 无效运行(){ 尝试 { 运行(); } 捕捉(可投掷的前) { log.log(完成?Level.INFO:Level.SEVERE, ex.toString()+": phantom ref poll 线程停止", ex); } 最后 { 运行=假; } } 私有易失性布尔完成=假; 私人无效doRun(){ 而(!完成){ 参考 ref = null; 尝试 { 运行=真; ref = refqueue.remove(); ķctl; 同步(参考地图){ ctl = refmap.remove(ref); 完成 = actCnt.decrementAndGet() == 0; if(log.isLoggable(Level.FINE)){ log.log(Level.FINE, "当前行为 refs={0}, mapsize={1}", new Object[]{actCnt.get(), refmap.size()}); } 如果(actCnt.get()!= refmap.size()){ Throwable ex = new IllegalStateException("活动引用计数和映射大小不同步"); log.log(Level.SEVERE, ex.toString(), ex); } } if(log.isLoggable(Level.FINER)){ log.log(Level.FINER, "参考发布: {0}, dep={1}", new Object[]{ref, ctl}); } 如果(ctl!= null){ 尝试 { 释放(ctl); if(log.isLoggable(Level.FINE)){ log.log(Level.FINE, "发布的依赖对象:{0}", ctl); } } 捕捉(运行时异常前){ log.log(Level.SEVERE, ex.toString(), ex); } } } 捕捉(异常前) { log.log(Level.SEVERE, ex.toString(), ex); } 最后 { 如果(参考!=空){ ref.clear(); } } } if(log.isLoggable(Level.FINE)){ log.log(Level.FINE, "poll thread {0} shutdown for {1}", new Object[]{mycnt, this}); } } } /** * 活动引用的计数。 */ 私有最终 AtomicInteger actCnt = new AtomicInteger(); /** * 从 T 引用映射到要用于释放(K)调用的 K 对象 */ private final ConcurrentHashMap,K>refmap = new ConcurrentHashMap,K>(); /** * 添加跟踪参考。 dep 不应以任何方式引用 ref,除非可能 * 弱引用。 dep 几乎总是 ref 所指的东西。 * @throws IllegalArgumentException of ref 和 dep 是同一个对象。 * @param dep 不再引用 ref 时需要清理的依赖对象。 * @param ref 要跟踪其引用的对象 */ 公共无效trackReference(T ref,K dep){ 如果(参考== dep){ throw new IllegalArgumentException("引用对象和依赖对象不能相同"); } PhantomReference p = new PhantomReference(ref, refqueue); 同步(参考地图){ refmap.put(p,dep); if(actCnt.getAndIncrement() == 0 || 运行 == false) { if(actCnt.get() > 0 && running == false) { if (log.isLoggable(Level.FINE)) { log.fine("启动停止的幻像 ref 轮询线程"); } } 轮询 = 新的 RefQueuePoll(); poll.start(); if(log.isLoggable(Level.FINE)){ log.log(Level.FINE, "为 {1} 创建的轮询线程 #{0}", new Object[]{tcnt.get(), this}); } } } } /** * 如果跟踪器所在的 JVM 正在被调用,则可以调用此方法 * 关闭,或其他上下文正在关闭并跟踪对象 * 现在应该由跟踪器发布。这种方法会导致 * {@link #released(Object) deleted(K) } 为每个未完成的引用调用。 */ 公共无效关机(){ 利斯特伦; // 复制值并清除地图以便释放 // 只被调用一次,以防 GC 稍后驱逐引用 同步(参考地图){ rem = new ArrayList(refmap.values()); refmap.clear(); } 为(K dep:rem){ 尝试 { 发布(dep); } 捕捉(异常前) { log.log(Level.SEVERE, ex.toString(), ex); } } } }

      【讨论】:

        【解决方案6】:

        它可以让你们两个拥有在内存管理方面非常有效的幻像缓存。 简而言之,如果您有创建成本高但很少使用的巨大对象,您可以使用幻像缓存来引用它们,并确保它们不会占用更有价值的内存。如果您使用常规引用,则必须手动确保没有对对象的引用。您可以对任何对象进行相同的争论,但您不必手动管理幻像缓存中的引用。只需要仔细检查它们是否已被收集。

        您也可以使用框架(即工厂),其中引用作为幻像引用给出。如果对象很多且寿命短(即使用然后处置),这将很有用。如果您有认为垃圾收集很神奇的草率程序员,清理内存非常方便。

        【讨论】:

        • 我认为你混淆了 SoftRefrences、WeakReferences 和 PhantomReferences
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-03-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-08-20
        • 1970-01-01
        • 2015-02-04
        相关资源
        最近更新 更多