2009-10-28 56 views
7

我正在使用一个搜索库,建议保持搜索句柄对象为此可以有利于查询缓存。在过去的一段时间里,我观察到缓存容易变得臃肿(几百megs,并且不断增长),并且OOM开始启动。无法强制实现缓存的限制,也无法计划它可以使用多少内存。所以我增加了限制,但这只是解决问题的一个临时解决方案。优雅地完成SoftReference对象

最终我在想这个物体是指代物java.lang.ref.SoftReference。因此,如果系统在空闲内存上运行不足,它会让对象继续运行,并根据需要创建新对象。这会在新的开始之后降低一些速度,但这比击中OOM更好。

我看到的关于SoftReferences的唯一问题是,没有清楚的方法让他们的参照物最终确定下来。在我的情况下,在销毁搜索句柄之前,我需要关闭它,否则系统可能会耗尽文件描述符。很明显,我可以将这个句柄包装到另一个对象中,在其上写入一个终结器(或挂接到一个ReferenceQueue/PhantomReference)并放手。但是,嘿,这个星球上的每一篇文章都建议不要使用终结器,尤其是 - 针对释放文件句柄的终结器(例如Effective Java ed。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和SoftReferences的片段,我越来越打印吨is null :(。但是如果我用MyReference替换代码(用MyReference取消注释并用SoftReference注释掉两行),我总是得到OOM。

正如我从建议中了解到的,在MyReference里面有硬引用不应该阻止对象打到ReferenceQueue,对不对?

回答

5

汤姆斯答案是正确的,但已添加到问题的代码是不一样的东西,提出了由汤姆。汤姆提出的看起来更像这样:

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来说明如何关闭Reader的概念,该Reader用于表示需要释放的外部资源。在这种特殊情况下,关闭该流实际上是无操作的,因此不需要,但是如果需要的话它会显示如何操作。

+0

有没有可能,因此编译?如MyReference构造函数接受鼓胀症指涉参数,应该将它分配给hardRef,但hardRef是一个完全不同的类型(ResourceThatMustBeClosed)的修复您的代码。此外,还可以解释为何膨胀是还是有必要的,一旦我们得到了ResourceThatMustBeClosed PS我不会这么贫困,如果这个问题已经不附带任何加分:P – mindas 2009-12-04 12:48:59

+0

我已经更新了代码,(希望)增加了一个明确的解释它是如何工作的?如果没有,请让我知道... – 2009-12-04 14:20:11

+0

代码是固定的,以便它编译。只需将它放入一个空类,然后添加适当的进口。 – 2009-12-04 21:10:58

7

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

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

(我的答案#1000。发布从伦敦DevDay。)

+1

油滑。 15 15 15 15 15. – 2009-10-28 17:56:32

+1

我很惊讶它在一个小时左右的睡眠之后是远程连贯的(是吗?),一天在黑暗的房间里(可怜的咖啡服务通过wifi工作),并试图听一个扬声器。但它必须完成。 – 2009-10-28 20:07:26

+0

汤姆,你能否请(或编辑这一个)更详细的答案,最终伴随着一些(伪)代码?我也有一个艰难的一天,也许明天我会更好地理解,但现在,不幸的是,我似乎不能。 – 2009-10-28 21:38:32

2

AHM。
(据我所知)你不能从两端拿着棍子。要么你坚持你的信息,要么你放手。
但是......你可以坚持一些关键信息,使你能够最终确定。当然,关键信息必须明显小于“真实信息”,并且不得在其可达对象图中包含真实信息(弱引用可能会帮助您)。
建立在现有的例子(注意关键信息字段):

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:
实际上,第二个周期会发生 - 但对于您的“关键数据”,而不是您的“主要膨胀数据”。实际数据将在第一个周期被清除。显然,真正的解决方案将使用一个单独的线程从引用队列中移除(不要poll(),remove(),在专用线程上阻塞)。

+0

忘了提及 - 用-Xmx 10mb运行这个例子不会产生OOM,并列出所有类型的数字(假定为“关键信息”)。 – 2009-12-06 15:19:59

0

@保罗 - 非常感谢你的回答和澄清。

@Ran - 我认为在你当前的代码中我在循环结束时缺少i ++。此外,您不需要在循环中执行rq.remove(),因为rq.poll()已经删除了顶部引用,不是吗?

几点:

1)我不得不添加了Thread.sleep(1)声明后,我在环(保罗和RAN)的这两个解决方案++来避免OOM,但是这无关大局,是也取决于平台。我的机器具有四核CPU,并且正在运行Sun Linux 1.6.0_16 JDK。

2)看着这些解决方案后,我想我会坚持使用终结器。 Bloch的书提供了以下原因:

  • 不保证终结器会立即执行,因此从不做任何时间关键的终结器 - 也没有任何保证SoftRererences!
  • 永远不要依赖终结器来更新关键的持久状态 - 我不是
  • 使用终结器会有严重的性能损失 - 在最糟糕的情况下,我会每分钟左右完成一个单个对象的定稿。我想我可以忍受这一点。
  • 使用try/finally - 哦,是的,我一定会!

有必要创造大量的脚手架只是看起来很简单的任务对我来说不合理。 我的意思是,从字面上看,任何其他查看此类代码的人的WTF每分钟费率都会很高。 3)遗憾的是,保罗,汤姆和Ran之间没有办法分开点:( 我希望汤姆不会介意,因为他已经有很多了:)在Paul和Ran之间判断要难得多 - 我认为这两个答案的工作是正确的。我只为Paul的答案设置了接受标志,因为它的评分更高(并且有更详细的解释),但是Ran的解决方案并不差,如果我选择使用SoftReferences实现它,可能会是我的选择。多谢你们!

+0

我++ - 是的,可能没有通过复制/粘贴。 不需要删除() - 正确。我错过了一半的参考资料。 – 2009-12-07 12:20:36