Java理論和實踐:用軟引用阻止記憶體流失

來源:互聯網
上載者:User
在本文中,他將解釋 Reference 對象的另外一種形式,即軟引用(soft references),用於協助垃圾收集器管理記憶體使用量和消除潛在的記憶體流失。

  垃圾收集可以使 Java 程式不會出現記憶體流失,至少對於比較狹窄的 “記憶體流失” 定義來說如此,但是這並不意味著我們可以完全忽略 Java 程式中的物件存留期(lifetime)問題。當我們沒有對對象生命週期(lifecycle)引起足夠的重視或者破壞了管理對象生命週期的標準機制時,Java 程式中通常就會出現記憶體流失。例如,上一次 我們看到了,不能劃分對象的生命週期會導致,在試圖將中繼資料關聯到瞬時對象時出現意外的對象保持。還有一些其他的情況可以類似地忽略或破壞對象生命週期管理,並導致記憶體流失。

  對象游離

  一種形式的記憶體流失有時候叫做對象游離(object loitering),是通過清單 1 中的 LeakyChecksum 類來說明的,清單 1 中有一個 getFileChecksum() 方法用於計算檔案內容的校正和。getFileChecksum() 方法將檔案內容讀取到緩衝區中以計算校正和。一種更加直觀的實現簡單地將緩衝區作為 getFileChecksum() 中的本地變數分配,但是該版本比那樣的版本更加 “聰明”,不是將緩衝區快取在執行個體欄位中以減少記憶體 churn。該 “最佳化”通常不帶來預期的好處;對象分配比很多人期望的更便宜。(還要注意,將緩衝區從本地變數提升到執行個體變數,使得類若不帶有附加的同步,就不再是安全執行緒的了。直觀的實現不需要將 getFileChecksum() 聲明為 synchronized,並且會在同時調用時提供更好的延展性。)

  清單 1. 展示 “對象游離” 的類

// BAD CODE - DO NOT EMULATE
public class LeakyChecksum {
 private byte[] byteArray;

 public synchronized int getFileChecksum(String fileName) {
  int len = getFileSize(fileName);
  if (byteArray == null || byteArray.length < len)
   byteArray = new byte[len];
  readFileContents(fileName, byteArray);
  // calculate checksum and return it
 }
}

  這個類存在很多的問題,但是我們著重來看記憶體流失。緩衝緩衝區的決定很可能是根據這樣的假設得出的,即該類將在一個程式中被調用許多次,因此它應該更加有效,以重用緩衝區而不是重新分配它。但是結果是,緩衝區永遠不會被釋放,因為它對程式來說總是可及的(除非 LeakyChecksum 對象被垃圾收集了)。更壞的是,它可以增長,卻不可以縮小,所以 LeakyChecksum 將永久保持一個與所處理的最大檔案一樣大小的緩衝區。退一萬步說,這也會給垃圾收集器帶來壓力,並且要求更頻繁的收集;為計算未來的校正和而保持一個大型緩衝區並不是可用記憶體的最有效利用。

  LeakyChecksum 中問題的原因是,緩衝區對於 getFileChecksum() 操作來說邏輯上是本地的,但是它的生命週期已經被人為延長了,因為將它提升到了執行個體欄位。因此,該類必須自己管理緩衝區的生命週期,而不是讓 JVM 來管理。

  軟引用

  弱引用如何可以給應用程式提供當對象被程式使用時另一種到達該對象的方法,但是不會延長對象的生命週期。Reference 的另一個子類 —— 軟引用 —— 可滿足一個不同卻相關的目的。其中弱引用允許應用程式建立不妨礙垃圾收集的引用,軟引用允許應用程式通過將一些對象指定為 “expendable” 而利用垃圾收集器的協助。儘管垃圾收集器在找出哪些記憶體在由應用程式使用哪些沒在使用方面做得很好,但是確定可用記憶體的最適當使用還是取決於應用程式。如果應用程式做出了不好的決定,使得對象被保持,那麼效能會受到影響,因為垃圾收集器必須更加辛勤地工作,以防止應用程式消耗掉所有記憶體。

  快取是一種常見的效能最佳化,允許應用程式重用以前的計算結果,而不是重新進行計算。快取是 CPU 利用和記憶體使用量之間的一種折衷,這種折衷理想的平衡狀態取決於有多少記憶體可用。若快取太少,則所要求的效能優勢無法達到;若太多,則效能會受到影響,因為太多的記憶體被用於快取上,導致其他用途沒有足夠的可用記憶體。因為垃圾收集器比應用程式更適合決定記憶體需求,所以應該利用垃圾收集器在做這些決定方面的協助,這就是件引用所要做的。

  如果一個對象惟一剩下的引用是弱引用或軟引用,那麼該對象是軟可及的(softly reachable)。垃圾收集器並不像其收集弱可及的對象一樣盡量地收集軟可及的對象,相反,它只在真正 “需要” 記憶體時才收集軟可及的對象。軟引用對於垃圾收集器來說是這樣一種方式,即 “只要記憶體不太緊張,我就會保留該對象。但是如果記憶體變得真正緊張了,我就會去收集並處理這個對象。” 垃圾收集器在可以拋出 OutOfMemoryError 之前需要清除所有的軟引用。

  通過使用一個軟引用來管理快取的緩衝區,可以解決 LeakyChecksum 中的問題,如清單 2 所示。現在,只要不是特別需要記憶體,緩衝區就會被保留,但是在需要時,也可被垃圾收集器回收:

  清單 2. 用軟引用修複 LeakyChecksum

public class CachingChecksum {
 private SoftReference<byte[]> bufferRef;

 public synchronized int getFileChecksum(String fileName) {
  int len = getFileSize(fileName);
  byte[] byteArray = bufferRef.get();
  if (byteArray == null || byteArray.length < len) {
   byteArray = new byte[len];
   bufferRef.set(byteArray);
  }
  readFileContents(fileName, byteArray);
  // calculate checksum and return it
 }
}

 一種廉價的緩衝

  CachingChecksum 使用一個軟引用來緩衝單個對象,並讓 JVM 處理從緩衝中取走對象時的細節。類似地,軟引用也經常用於 GUI 應用程式中,用於緩衝位元影像圖形。是否可使用軟引用的關鍵在於,應用程式是否可從大量緩衝的資料恢複。

  如果需要緩衝不止一個對象,您可以使用一個 Map,但是可以選擇如何使用軟引用。您可以將緩衝作為 Map<K, SoftReference<V>> 或 SoftReference<Map<K,V>> 管理。後一種選項通常更好一些,因為它給垃圾收集器帶來的工作更少,並且允許在特別需要記憶體時以較少的工作回收整個緩衝。弱引用有時會錯誤地用於取代軟引用,用於構建緩衝,但是這會導致差的緩衝效能。在實踐中,弱引用將在對象變得弱可及之後被很快地清除掉 —— 通常是在緩衝的對象再次用到之前 —— 因為小的垃圾收集運行得很頻繁。

  對於在效能上非常依賴快取的應用程式來說,軟引用是一個不管用的手段,它確實不能取代能夠提供靈活終止期、複製和事務型快取的複雜的快取架構。但是作為一種 “廉價(cheap and dirty)” 的快取機制,它對於降低價格是很有吸引力的。

  正如弱引用一樣,軟引用也可建立為具有一個相關的引用隊列,引用在被垃圾收集器清除時進入隊列。引用隊列對於軟引用來說,沒有對弱引用那麼有用,但是它們可以用於發出管理警報,說明應用程式開始缺少記憶體。

  垃圾收集器如何處理 References

  弱引用和軟引用都擴充了抽象的 Reference 類(虛引用(phantom references)也一樣,這將在以後的文章中介紹)。引用對象被垃圾收集器特殊地看待。垃圾收集器在跟蹤堆期間遇到一個 Reference 時,不會標記或跟蹤該引用對象,而是在已知活躍的 Reference 對象的隊列上放置一個 Reference。在跟蹤之後,垃圾收集器就識別軟可及的對象 —— 這些對象上除了軟引用外,沒有任何強引用。垃圾收集器然後根據當前收集所回收的記憶體總量和其他策略考慮因素,判斷軟引用此時是否需要被清除。將被清除的軟引用如果具有相應的引用隊列,就會進入隊列。其餘的軟可及對象(沒有清除的對象)然後被看作一個根集(root set),堆跟蹤繼續使用這些新的根,以便通過活躍的軟引用而可及的對象能夠被標記。

  處理軟引用之後,弱可及對象的集合被識別 —— 這樣的對象上不存在強引用或軟引用。這些對象被清除和排入佇列。所有 Reference 類型在排入佇列之前被清除,所以處理事後檢查(post-mortem)清除的線程永遠不會具有 referent 對象的訪問權,而只具有 Reference 對象的訪問權。因此,當 References 與引用隊列一起使用時,通常需要細分適當的參考型別,並將它直接用於您的設計中(與 WeakHashMap 一樣,它的 Map.Entry 擴充了 WeakReference)或者儲存對需要清除的實體的引用。

  引用處理的效能成本

  引用對象給垃圾收集過程帶來了一些附加的成本。每一次垃圾收集,都必須構造活躍 Reference 對象的一個列表,而且每個引用都必須做適當的處理,這給每次收集添加了一些每個 Reference 的開銷,而不管該 referent 此時是否被收集。Reference 對象本身服從於垃圾收集,並且可在 referent 之前被收集,在這樣的情況下,它們沒有排入佇列。

  基於數組的集合

  當數組用於實現諸如堆棧或環形緩衝區之類的資料結構時,會出現另一種形式的對象游離。清單 3 中的 LeakyStack 類展示了用數組實現的堆棧的實現。在 pop() 方法中,在頂部指標遞減之後,elements 仍然會保留對將彈出堆棧的對象的引用。這意味著,該對象的引用對程式來說仍然可及(即使程式實際上不會再使用該引用),這會阻止該對象被垃圾收集,直到該位置被未來的 push() 重用。

  清單 3. 基於數組的集合中的對象游離

public class LeakyStack {
 private Object[] elements = new Object[MAX_ELEMENTS];
 private int size = 0;

 public void push(Object o) { elements[size++] = o; }
 
 public Object pop() {
  if (size == 0)
   throw new EmptyStackException();
  else {
   Object result = elements[--size];
   // elements[size+1] = null;
  return result;
 }
}
}

  修複這種情況下的對象游離的方法是,當對象從堆棧彈出之後,就消除它的引用,如清單 3 中注釋掉的行所示。但是這種情況 —— 由類管理其自己的記憶體 —— 是一種非常少見的情況,即顯式地消除不再需要的對象是一個好主意。大部分時候,認為不應該使用的強行消除引用根本不會帶來效能或記憶體使用量方面的收益,通常是導致更差的效能或者 NullPointerException。該演算法的一個連結實現不會存在這個問題。在連結實現中,連結節點(以及所儲存的對象的引用)的生命期將被自動與Object Storage Service在集合中的期間綁定在一起。弱引用可用於解決這個問題 —— 維護弱引用而不是強引用的一個數組 —— 但是在實際中,LeakyStack 管理它自己的記憶體,因此負責確保對不再需要的對象的引用被清除。使用數組來實現堆棧或緩衝區是一種最佳化,可以減少分配,但是會給實現者帶來更大的負擔,需要仔細地管理儲存在數組中的引用的生命期。

  結束語

  與弱引用一樣,軟引用通過利用垃圾收集器在作出緩衝回收決策方面的協助,有助於防止應用程式出現對象游離。只有當應用程式可以忍受大量軟引用的對象時,軟引用才適合使用。

 

本文轉自: http://dev.yesky.com/124/2341124.shtml

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.