2009-10-28 30 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