优雅地完成SoftReference指示

时间:2009-10-28 17:37:59

标签: java finalizer finalize soft-references

我正在使用一个搜索库,建议保持搜索句柄对象打开,这样可以使查询缓存受益。随着时间的推移,我发现缓存容易变得臃肿(几百兆并且不断增长),并且OOM开始启动。没有办法强制执行此缓存的限制,也没有计划它可以使用多少内存。所以我增加了 Xmx 限制,但这只是问题的临时解决方案。

最终我想将此对象设为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,对吗?

4 个答案:

答案 0 :(得分:7)

对于有限数量的资源:子类SoftReference。软引用应指向封闭对象。子类中的强引用应引用资源,因此始终可以很容易地访问它。通过ReferenceQueue poll读取时,可以关闭资源并从缓存中删除资源。需要正确释放缓存(如果SoftReference本身被垃圾收集,则无法将其排入ReferenceQueue)。

请注意,缓存中只有有限数量的资源未被释放 - 逐出旧条目(实际上,如果符合您的情况,您可以使用有限缓存丢弃软引用)。通常情况下,非内存资源更为重要,在这种情况下,没有外来参考对象的LRU-eviction缓存就足够了。

(我的回答#1000。发自伦敦DevDay。)

答案 1 :(得分: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缓存,以确保您只保留 stick in the air 500打开文件。它们也可以是MyReference类型,因此如果需要它们也可以被垃圾收集。

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

编辑:更新了代码,以便在投入课程时进行编译。它使用StringReader来说明如何关闭Reader的概念,Reader用于表示需要释放的外部资源。在这个特殊情况下,关闭该流实际上是一个无操作,因此不需要,但它显示了如果需要它的方法。

答案 2 :(得分: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()的用途。由于您声明您不希望第二个周期发生,因此您无法保留您的信息(如果 - > b,那么!b - &gt;!a)。这意味着你必须放手。

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

EDIT3:
显然,真正的解决方案是使用单独的线程从引用队列中删除(不要在专用线程上使用poll(),remove(),阻塞)。

答案 3 :(得分:0)

@Paul - 非常感谢答案和澄清。

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

几点:

1)我必须在循环中的i ++之后添加Thread.sleep(1)语句(对于Paul和Ran的两个解决方案)以避免OOM,但这与大图无关,并且还与平台相关。我的机器有一个四核CPU,运行的是Sun Linux 1.6.0_16 JDK。

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

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

有必要为看似简单的任务创造大量的脚手架对我来说看起来并不合理。 我的意思是,从字面上看,对于任何查看此类代码的人来说,每分钟的WTF率都会非常高。

3)可悲的是,Paul,Tom和Ran之间没有办法分割点数:( 我希望汤姆不介意,因为他已经有很多他们:)在保罗和冉之间判断更难 - 我认为这两个答案都有效并且是正确的。我只是为Paul的回答设置了接受标志,因为它被评为更高(并且有更详细的解释),但Ran的解决方案一点也不差,如果我选择使用SoftReferences实现它,可能是我的选择。谢谢你们!