9

我正在使用一个搜索库,它建议保持搜索句柄对象打开,这样可以使查询缓存受益。随着时间的推移,我观察到缓存趋于膨胀(几百兆并不断增长)并且 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

4 回答 4

7

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

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

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

于 2009-10-28T17:53:49.243 回答
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 保留了对需要关闭的内部资源的硬引用。

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

于 2009-12-03T22:11:26.397 回答
2

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

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)。这意味着你必须放手。

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

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

于 2009-12-06T15:19:11.760 回答
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)在查看了这些解决方案之后,我想我会坚持使用终结器。布洛赫的书提供了以下理由:

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

必须为看似简单的任务创建大量脚手架对我来说似乎不合理。我的意思是,从字面上看,对于其他查看此类代码的人来说,每分钟的 WTF 速率将非常高。

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

于 2009-12-07T11:46:50.840 回答