建議:消除過期的物件引用。

孤芳不自賞發表於2017-08-21

以下是簡單的棧實現的例子:

// Can you spot the "memory leak"?

public class Stack {

  private Object[] elements;

  private int size = 0;

  private static final int DEFAULT_INITIAL_CAPACITY = 16;


  public Stack() {

    elements = new Object[DEFAULT_INITIAL_CAPACITY];

  }

  public void push(Object e) {

    ensureCapacity();

    elements[size ++ ] = e;

  }

  public Object pop() {

    if (size == 0) throw new EmptyStackException();

    return elements[-- size];

  }

 /*

  * Ensure space for at least one more element,

  * roughly doubling the capacity each time the array needs to grow.

  */

  private void ensureCappacity() {

    if(elements.length == size)

      elements = Arrays.copy(elements, 2 * size + 1);

  }

}

        這段程式中並沒有很明顯的錯誤。無論如何測試,它都會成功的通過每一項測試,但是這個程式中隱藏著一個問題。不嚴格地講,這段程式有一個“記憶體洩漏”,隨著垃圾回收器活動的增加,或者由於記憶體佔用的不斷增加,程式效能的降低會逐漸表現出來。在極端的情況下,這種記憶體洩露會導致磁碟交換(Disk Paging),甚至導致程式失敗(OutOfMemoryError錯誤),但是這種失敗情形相對比較少見。

         那麼,程式中哪裡發生了記憶體洩漏?如果一個棧先是增長,然後再收縮,那麼,從棧中彈出來的物件將不會被當做垃圾回收,即使使用棧的程式不再引用這些物件,他們也不會被回收。這是因為,棧內部維護著對這些物件的過期引用(obsolete referrence)。所謂過期引用,是指永遠也不會再被解除的引用。在本例中,凡是在elements陣列的“活動部分(active portion)”之外的任何引用都是過期的。活動部分是指elements中下標小於size的哪些部分。

         在支援垃圾回收的語言中,記憶體洩漏是很隱蔽的(稱這類記憶體洩漏為“無意識的物件保持(unintentional object retention)”更為恰當)。如果一個物件引用被無意識地保留起來了,那麼,垃圾回收機制不僅不會處理這個物件,而且也不會處理被這個物件所引用的所有其他物件。即使只有少量的幾個物件引用被無意識的保留下來,他會有許許多多的物件被排除在垃圾回收機制之外,從而對效能造成潛在的重大影響。

        這類問題的修復方法很簡單:一旦物件引用已經過期,只需清空這些引用即可。

        對於上述例子中的Stack類而言,只要一個單元被彈出棧,指向他的引用過期了。pop方法的修訂版本如下所示:  

 public Object pop() {

    if (size == 0) throw new EmptyStackException();

   Object result = elements[-- size];

   elements[size] = null; // Elements obsolete reference

    return elements[-- size];

  }

        清空過期引用的另一個好處是,如果他們以後又被錯誤的解除引用,程式就會立即丟擲NullPointerException異常,而不是悄悄地錯誤執行下去,儘快的檢測出程式中的錯誤總是有意義的。

        清空物件引用應該是一種例外,而不是一種規範行為。因為過期引用最好的方法使讓包含該引用的變數結束其生命週期。如果你是在最緊湊的作用域範圍內定義每一個變數,這種情形就會自然而然的發生。

        那麼,何時應該清空引用?Stack類的哪方面特性使他易於遭受記憶體洩露的影響呢?簡而言之,問題在於,Stack類自己管理記憶體(manage its own memory)。儲存池(storage pool)包含了elements陣列(物件引用單元,而不是物件本身)的元素。陣列活動區域(同前面的定義)中的元素是已分配的(allocated),而陣列其餘部分的元素則是自由的(free)。但是垃圾回收器並不知道這一點,對於垃圾回收器而言,elements陣列中的所有物件引用都同等有效。只有程式設計師知道陣列的非活動部分是不重要的。程式設計師可以把這個情況告知垃圾回收器,做法很簡單:一旦陣列元素變成了非活動的一部分,程式設計師就手工清空這些陣列元素。

        一般而言,只要類是自己管理記憶體,程式設計師就應該警惕記憶體洩漏問題。一旦元素被釋放掉,則該元素中包含的任何物件引用都應該被清空。

       記憶體洩漏的另一個常見來源是快取。只要在快取之外存在對某個項的鍵的引用,該項就有意義,那麼就可以用WeakHashMap代表快取;當快取中的項過期之後,他們就會自動被刪除。記住只有當所有快取項的生命週期是由該鍵的外部引用而不是由值決定時,WeakHashMap才有用處。

      記憶體洩漏的第三個常見來源是監聽器和其他回撥。確保回撥立即被當做垃圾回收的最佳方法使只儲存他們的弱引用(weak reference),例如,只將它們儲存成WeakHashMap的鍵。

相關文章