有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

java优雅地完成SoftReference引用

我使用的是一个搜索库,它建议保持搜索句柄对象的打开状态,因为这样可以使查询缓存受益。随着时间的推移,我观察到缓存趋于膨胀(几百兆并不断增长),OOM开始启动。无法强制执行此缓存的限制,也无法计划它可以使用多少内存。所以我增加了Xmx限制,但这只是问题的临时解决方案

最终我想把这个对象变成^{参照物。因此,如果系统的可用内存不足,它会释放对象,并根据需要创建一个新的对象。这会在重新开始后降低一些速度,但这是一个比按OOM更好的选择

我所看到的关于软引用的唯一问题是,没有一种干净的方式来最终确定它们的引用。在我的例子中,在销毁搜索句柄之前,我需要关闭它,否则系统可能会耗尽文件描述符。显然,我可以将这个句柄包装到另一个对象中,在其上编写终结器(或者挂接到ReferenceQueue/PhantomReference)并释放它。但是,这个星球上的每一篇文章都建议不要使用终结器,尤其是用来释放文件句柄的终结器(例如,Effective Javaed.II,第27页)

所以我有点困惑。我是否应该小心地忽略所有这些建议,继续下去。否则,还有其他可行的替代方案吗?提前谢谢

编辑#1:下面的文本是在测试了Tom Hawtin建议的一些代码后添加的。在我看来,要么是建议不起作用,要么是我遗漏了什么。以下是代码:

class Bloat {  // just a heap filler really
   private double a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;

   private final int ii;

   public Bloat(final int ii) {
      this.ii = ii;
   }
}

// as recommended by Tom Hawtin
class MyReference<T> extends SoftReference<T> {
   private final T hardRef;

   MyReference(T referent, ReferenceQueue<? super T> q) {
      super(referent, q);
      this.hardRef = referent;
   }
}

//...meanwhile, somewhere in the neighbouring galaxy...
{
   ReferenceQueue<Bloat> rq = new ReferenceQueue<Bloat>();
   Set<SoftReference<Bloat>> set = new HashSet<SoftReference<Bloat>>();
   int i=0;

   while(i<50000) {
//      set.add(new MyReference<Bloat>(new Bloat(i), rq));
      set.add(new SoftReference<Bloat>(new Bloat(i), rq));

//      MyReference<Bloat> polled = (MyReference<Bloat>) rq.poll();
      SoftReference<Bloat> polled = (SoftReference<Bloat>) rq.poll();

      if (polled != null) {
         Bloat polledBloat = polled.get();
         if (polledBloat == null) {
           System.out.println("is null :(");
         } else {
           System.out.println("is not null!");
         }
      }
      i++;
   }
}

如果我用-Xmx10m和软引用(如上面的代码)运行上面的代码段,我会打印出大量的is null :(。但是如果我用MyReference替换代码(用MyReference取消两行的注释,用SoftReference注释掉一行),我总是会得到OOM

正如我从建议中了解到的,在MyReference内有硬引用不应该阻止对象击中ReferenceQueue,对吗


共 (4) 个答案

  1. # 1 楼答案

    @Paul-非常感谢您的回答和澄清

    @Ran-我认为在你当前的代码中,循环的末尾缺少了I++。而且,你不需要做rq。在循环中删除()作为rq。poll()已经删除了顶级引用,不是吗

    几点:

    1)我必须添加线程。在i++进入循环后的sleep(1)语句(对于Paul和Ran的两种解决方案),以避免OOM,但这与全局无关,也取决于平台。我的机器有一个四核CPU,运行的是Sun Linux 1.6.0_16 JDK

    2)看过这些解决方案后,我想我会坚持使用终结器。布洛赫的书提供了以下理由:

    • 不能保证终结器会立即执行,因此永远不要在终结器中执行任何时间关键的操作——也不能保证软引用
    • 永远不要依赖终结器来更新关键的持久状态——我不是
    • 使用终结器会带来严重的性能损失——在我最糟糕的情况下,大约每分钟终结一个对象。我想我可以接受
    • 使用try/finally--哦,是的,我一定会的

    仅仅为了一项看似简单的任务就必须建造大量脚手架,这在我看来是不合理的。 我的意思是,从字面上说,WTF每分钟的速率对于任何其他查看此类代码的人来说都是相当高的

    3)可悲的是,保罗、汤姆和兰之间没有办法分得分数:( 我希望汤姆不会介意,因为他已经得到了很多答案:)在保罗和兰之间做出判断要困难得多——我认为两个答案都是正确的。我只是将accept flag设置为Paul的答案,因为它的评分更高(并且有更详细的解释),但Ran的解决方案一点也不差,如果我选择使用软引用实现它,它可能是我的选择。谢谢大家

  2. # 2 楼答案

    Tom的答案是正确的,但是添加到问题中的代码与Tom提出的不同。汤姆的提议更像这样:

    class Bloat {  // just a heap filler really
        public Reader res;
        private double a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;
    
        private final int ii;
    
        public Bloat(final int ii, Reader res) {
           this.ii = ii;
           this.res = res;
        }
     }
    
     // as recommended by Tom Hawtin
     class MySoftBloatReference extends SoftReference<Bloat> {
        public final Reader hardRef;
    
        MySoftBloatReference(Bloat referent, ReferenceQueue<Bloat> q) {
           super(referent, q);
           this.hardRef = referent.res;
        }
     }
    
     //...meanwhile, somewhere in the neighbouring galaxy...
     {
        ReferenceQueue<Bloat> rq = new ReferenceQueue<Bloat>();
        Set<SoftReference<Bloat>> set = new HashSet<SoftReference<Bloat>>();
        int i=0;
    
        while(i<50000) {
            set.add(new MySoftBloatReference(new Bloat(i, new StringReader("test")), rq));
    
            MySoftBloatReference polled = (MySoftBloatReference) rq.poll();
    
            if (polled != null) {
                // close the reference that we are holding on to
                try {
                    polled.hardRef.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            i++;
        }
    }
    

    请注意,最大的区别在于硬引用指向需要关闭的对象。周围的对象可以,也将被垃圾收集,所以你不会撞到OOM,但是你仍然有机会关闭引用。一旦你离开循环,它也会被垃圾收集。当然,在现实世界中,您可能不会使res成为公共实例成员

    这就是说,如果您持有打开的文件引用,那么在内存耗尽之前,您就面临着耗尽这些引用的风险。您可能还希望有一个LRU缓存,以确保在空中保存的文件不超过500个。它们也可以是MyReference类型,以便在需要时也可以进行垃圾收集

    为了稍微说明MySoftBloatReference是如何工作的,基类SoftReference仍然持有对占用所有内存的对象的引用。这是需要释放的对象,以防止OOM发生。但是,如果对象被释放,您仍然需要释放Bloat正在使用的资源,也就是说,Bloat正在使用两种类型的资源,内存和文件句柄,这两种资源都需要被释放,或者一种或另一种资源用完。SoftReference通过释放该对象来处理对内存资源的压力,但是您还需要释放另一个资源,即文件句柄。由于Bloat已被释放,我们无法使用它来释放相关资源,因此MySoftBloatReference保留了对需要关闭的内部资源的硬引用。一旦被告知Bloat已被释放,即一旦引用出现在ReferenceQueue中,MySoftBloatReference也可以通过其拥有的硬引用关闭相关资源

    编辑:更新了代码,以便在放入类时进行编译。它使用StringReader来说明如何关闭读卡器的概念,该读卡器用于表示需要释放的外部资源。在这种特殊情况下,关闭该流实际上是一种不操作,因此不需要,但它展示了如何在需要时这样做

  3. # 3 楼答案


    (据我所知)你不能两手空空。你要么保留你的信息,要么放手
    然而您可以保留一些关键信息,以便最终确定。当然,关键信息必须比“真实信息”小得多,并且不能在其可到达的对象图中包含真实信息(弱引用可能会有帮助)
    以现有示例为基础(注意关键信息字段):

    public class Test1 {
        static class Bloat {  // just a heap filler really
            private double a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z;
    
            private final int ii;
    
            public Bloat(final int ii) {
                this.ii = ii;
            }
        }
    
        // as recommended by Tom Hawtin
        static class MyReference<T, K> extends SoftReference<T> {
            private final K keyInformation;
    
            MyReference(T referent, K keyInformation, ReferenceQueue<? super T> q) {
                super(referent, q);
                this.keyInformation = keyInformation;
            }
    
            public K getKeyInformation() {
                return keyInformation;
            }
        }
    
        //...meanwhile, somewhere in the neighbouring galaxy...
        public static void main(String[] args) throws InterruptedException {
            ReferenceQueue<Bloat> rq = new ReferenceQueue<Bloat>();
            Set<SoftReference<Bloat>> set = new HashSet<SoftReference<Bloat>>();
            int i = 0;
    
            while (i < 50000) {
                set.add(new MyReference<Bloat, Integer>(new Bloat(i), i, rq));
    
                final Reference<? extends Bloat> polled = rq.poll();
    
                if (polled != null) {
                    if (polled instanceof MyReference) {
                        final Object keyInfo = ((MyReference) polled).getKeyInformation();
                        System.out.println("not null, got key info: " + keyInfo + ", finalizing...");
                    } else {
                        System.out.println("null, can't finalize.");
                    }
                    rq.remove();
                    System.out.println("removed reference");
                }
    

    编辑:
    我想详细说明一下“要么保留信息,要么放手”。假设你有办法掌握你的信息。这将迫使GC取消标记您的数据,导致只有在您处理完数据后,才在第二个GC周期内真正清理数据。这是可能的——这正是finalize()的用途。由于您声明不希望第二个周期发生,因此无法保存您的信息(如果a-->;b那么!b-->;!a)。这意味着你必须放手

    编辑2:
    实际上,第二个周期会发生——但对于你的“关键数据”,而不是你的“主要膨胀数据”。实际数据将在第一个周期清除

    编辑3:
    显然,真正的解决方案将使用一个单独的线程从引用队列中移除(不要轮询()、移除()、在专用线程上阻塞)

  4. # 4 楼答案

    对于有限数量的资源:子类SoftReference。软引用应指向封闭对象。子类中的强引用应该引用资源,因此它总是强可访问的。当通过ReferenceQueuepoll读取时,可以关闭资源并将其从缓存中删除。需要正确释放缓存(如果SoftReference本身是垃圾收集的,则不能将其排入ReferenceQueue的队列)

    请注意,缓存中只有有限数量的未释放资源——逐出旧条目(实际上,如果适合您的情况,您可以使用if finite cache丢弃软引用)。通常情况下,更重要的是非内存资源,在这种情况下,没有外来引用对象的LRU逐出缓存应该足够了

    (我的答案是#1000。伦敦星期五发布。)