程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java理論和實踐:用軟引用阻止內存洩漏

Java理論和實踐:用軟引用阻止內存洩漏

編輯:關於JAVA
在本文中,他將解釋 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。該算法的一個鏈接實現不會存在這個問題。在鏈接實現中,鏈接節點(以及所存儲的對象的引用)的生命期將被自動與對象存儲在集合中的期間綁定在一起。弱引用可用於解決這個問題 —— 維護弱引用而不是強引用的一個數組 —— 但是在實際中,LeakyStack 管理它自己的內存,因此負責確保對不再需要的對象的引用被清除。使用數組來實現堆棧或緩沖區是一種優化,可以減少分配,但是會給實現者帶來更大的負擔,需要仔細地管理存儲在數組中的引用的生命期。 

  結束語

  與弱引用一樣,軟引用通過利用垃圾收集器在作出緩存回收決策方面的幫助,有助於防止應用程序出現對象游離。只有當應用程序可以忍受大量軟引用的對象時,軟引用才適合使用。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved