1. 程式人生 > >Java記憶體模式

Java記憶體模式

public class VectorMemoryLeak {
    public static void main(String args[]){
        Vector<String> vector = new Vector<String>();
        for( int i = 0; i < 1000; i++ ){
            String tempString = new String();
            vector.add(tempString);
            tempString = null;
        }
    }
}
  從上邊這個例子可以看到,迴圈申請了String物件,並且將申請的物件放入了一個Vector中,如果僅僅是釋放物件本身,因為Vector仍然引用了該物件,所以這個物件對CG來說是不可回收的,因此如果物件加入到Vector後,還必須從Vector刪除才能夠回收,最簡單的方式是將Vector引用設定成null。實際上這些物件已經沒有用了,但是還是被程式碼裡面的引用引用到了,這種情況GC拿它就沒有了任何辦法,這樣就可以導致了記憶體洩漏。
  【*:Java語言因為提供了垃圾回收器,照理說是不會出現記憶體洩漏的,Java裡面導致記憶體洩漏的主要原因就是,先前申請了記憶體空間而忘記了釋放。如果程式中存在對無用物件的引用,這些物件就會駐留在記憶體中消耗記憶體,因為無法讓GC判斷這些物件是否可達。如果存在物件的引用,這個物件就被定義為“有效的活動狀態”,同時不會被釋放,要確定物件所佔記憶體被回收,必須要確認該物件不再被使用。典型的做法就是把物件資料成員設定成為null或者中集合中移除,當局部變數不需要的情況則不需要顯示宣告為null。】
  ii.常見的Java記憶體洩漏
  1)全域性集合:
  在大型應用程式中存在各種各樣的全域性資料倉庫是很普遍的,比如一個JNDI樹或者一個Session table(會話表),在這些情況下,必須注意管理儲存庫的大小,必須有某種機制從儲存庫中移除不再需要的資料。
  [$]解決:
  [1]常用的解決方法是週期運作清除作業,該作業會驗證倉庫中的資料然後清楚一切不需要的資料
  [2]另外一種方式是反向連結計數,集合負責統計集合中每個入口的反向連結資料,這要求反向連結告訴集合合適會退出入口,當反向連結數目為零的時候,該元素就可以移除了。
  2)快取:
  快取一種用來快速查詢已經執行過的操作結果的資料結構。因此,如果一個操作執行需要比較多的資源並會多次被使用,通常做法是把常用的輸入資料的操作結果進行快取,以便在下次呼叫該操作時使用快取的資料。快取通常都是以動態方式實現的,如果快取設定不正確而大量使用快取的話則會出現記憶體溢位的後果,因此需要將所使用的記憶體容量與檢索資料的速度加以平衡。
  [$]解決:
  [1]常用的解決途徑是使用java.lang.ref.SoftReference類堅持將物件放入快取,這個方法可以保證當虛擬機器用完記憶體或者需要更多堆的時候,可以釋放這些物件的引用。
  3)類載入器:
  Java類裝載器的使用為記憶體洩漏提供了許多可乘之機。一般來說類裝載器都具有複雜結構,因為類裝載器不僅僅是隻與"常規"物件引用有關,同時也和物件內部的引用有關。比如資料變數,方法和各種類。這意味著只要存在對資料變數,方法,各種類和物件的類裝載器,那麼類裝載器將駐留在JVM中。既然類裝載器可以同很多的類關聯,同時也可以和靜態資料變數關聯,那麼相當多的記憶體就可能發生洩漏。
  iii.Java引用【摘錄自前邊的《Java引用總結》】:
  Java中的物件引用主要有以下幾種型別:
  1)強可及物件(strongly reachable):
  可以通過強引用訪問的物件,一般來說,我們平時寫程式碼的方式都是使用的強引用物件,比如下邊的程式碼段:
  StringBuilder builder= new StringBuilder();
  上邊程式碼部分引用obj這個引用將引用記憶體堆中的一個物件,這種情況下,只要obj的引用存在,垃圾回收器就永遠不會釋放該物件的儲存空間。這種物件我們又成為強引用(Strong references),這種強引用方式就是Java語言的原生的Java引用,我們幾乎每天程式設計的時候都用到。上邊程式碼JVM儲存了一個StringBuilder型別的物件的強引用在變數builder呢。強引用和GC的互動是這樣的,如果一個物件通過強引用可達或者通過強引用鏈可達的話這種物件就成為強可及物件,這種情況下的物件垃圾回收器不予理睬。如果我們開發過程不需要垃圾回器回收該物件,就直接將該物件賦為強引用,也是普通的程式設計方法。
  2)軟可及物件(softly reachable):
  不通過強引用訪問的物件,即不是強可及物件,但是可以通過軟引用訪問的物件就成為軟可及物件,軟可及物件就需要使用類SoftReference(java.lang.ref.SoftReference)。此種類型的引用主要用於記憶體比較敏感的快取記憶體,而且此種引用還是具有較強的引用功能,當記憶體不夠的時候GC會回收這類記憶體,因此如果記憶體充足的時候,這種引用通常不會被回收的。不僅僅如此,這種引用物件在JVM裡面保證在丟擲OutOfMemory異常之前,設定成為null。通俗地講,這種型別的引用保證在JVM記憶體不足的時候全部被清除,但是有個關鍵在於:垃圾收集器在執行時是否釋放軟可及物件是不確定的,而且使用垃圾回收演算法並不能保證一次性尋找到所有的軟可及物件。當垃圾回收器每次執行的時候都可以隨意釋放不是強可及物件佔用的記憶體,如果垃圾回收器找到了軟可及物件過後,可能會進行以下操作:
將SoftReference物件的referent域設定成為null,從而使該物件不再引用heap物件。
SoftReference引用過的記憶體堆上的物件一律被生命為finalizable。
當記憶體堆上的物件finalize()方法被執行而且該物件佔用的記憶體被釋放,SoftReference物件就會被新增到它的ReferenceQueue,前提條件是ReferenceQueue本身是存在的。
  既然Java裡面存在這樣的物件,那麼我們在編寫程式碼的時候如何建立這樣的物件呢?建立步驟如下:
  先建立一個物件,並使用普通引用方式【強引用】,然後再建立一個SoftReference來引用該物件,最後將普通引用設定為null,通過這樣的方式,這個物件就僅僅保留了一個SoftReference引用,同時這種情況我們所建立的物件就是SoftReference物件。一般情況下,我們可以使用該引用來完成Cache功能,就是前邊說的用於快取記憶體,保證最大限度使用記憶體而不會引起記憶體洩漏的情況。下邊的程式碼段:
  public static void main(String args[])
  {
    //建立一個強可及物件
    A a = new A();
    //建立這個物件的軟引用SoftReference
    SoftReference sr = new SoftReference(a);
    //將強引用設定為空,以遍垃圾回收器回收強引用
    a = null;
    //下次使用該物件的操作
    if( sr != null ){
      a = (A)sr.get();
    }else{
      //這種情況就是由於記憶體過低,已經將軟引用釋放了,因此需要重新裝載一次
      a = new A();
      sr = new SoftReference(a);
    }
  }
  軟引用技術使得Java系統可以更好地管理記憶體,保持系統穩定,防止記憶體洩漏,避免系統崩潰,因此在處理一些記憶體佔用大而且生命週期長使用不頻繁的物件可以使用該技術。
  3)弱可及物件(weakly reachable):
  不是強可及物件同樣也不是軟可及物件,僅僅通過弱引用WeakReference(java.lang.ref.WeakReference)訪問的物件,這種物件的用途在於規範化對映(canonicalized mapping),對於生存週期相對比較長而且重新建立的時候開銷少的物件,弱引用也比較有用,和軟引用物件不同的是,垃圾回收器如果碰到了弱可及物件,將釋放WeakReference物件的記憶體,但是垃圾回收器需要執行很多次才能夠找到弱可及物件。弱引用物件在使用的時候,可以配合ReferenceQueue類使用,如果弱引用被回收,JVM就會把這個弱引用加入到相關的引用佇列中去。最簡單的弱引用方法如以下程式碼:
  WeakReference weakWidget = new WeakReference(classA);
  在上邊程式碼裡面,當我們使用weakWidget.get()來獲取classA的時候,由於弱引用本身是無法阻止垃圾回收的,所以我們也許會拿到一個null為返回。【*:這裡提供一個小技巧,如果我們希望取得某個物件的資訊,但是又不影響該物件的垃圾回收過程,我們就可以使用WeakReference來記住該物件,一般我們在開發偵錯程式和優化器的時候使用這個是很好的一個手段。】
  如果上邊的程式碼部分,我們通過weakWidget.get()返回的是null就證明該物件已經被垃圾回收器回收了,而這種情況下弱引用物件就失去了使用價值,GC就會定義為需要進行清除工作。這種情況下弱引用無法引用任何物件,所以在JVM裡面就成為了一個死引用,這就是為什麼我們有時候需要通過ReferenceQueue類來配合使用的原因,使用了ReferenceQueue過後,就使得我們更加容易監視該引用的物件,如果我們通過一ReferenceQueue類來構造一個弱引用,當弱引用的物件已經被回收的時候,系統將自動使用物件引用佇列來代替物件引用,而且我們可以通過ReferenceQueue類的執行來決定是否真正要從垃圾回收器裡面將該死引用(Dead Reference)清除。
  弱引用程式碼段:
  //建立普通引用物件
  MyObject object = new MyObject();
  //建立一個引用佇列
  ReferenceQueue rq = new ReferenceQueue();
  //使用引用佇列建立MyObject的弱引用
  WeakReference wr = new WeakReference(object,rq);
  這裡提供兩個實在的場景來描述弱引用的相關用法:
  [1]你想給物件附加一些資訊,於是你用一個 Hashtable 把物件和附加資訊關聯起來。你不停的把物件和附加資訊放入 Hashtable 中,但是當物件用完的時候,你不得不把物件再從 Hashtable 中移除,否則它佔用的記憶體變不會釋放。萬一你忘記了,那麼沒有從 Hashtable 中移除的物件也可以算作是記憶體洩漏。理想的狀況應該是當物件用完時,Hashtable 中的物件會自動被垃圾收集器回收,不然你就是在做垃圾回收的工作。
  [2]你想實現一個圖片快取,因為載入圖片的開銷比較大。你將圖片物件的引用放入這個快取,以便以後能夠重新使用這個物件。但是你必須決定快取中的哪些圖片不再需要了,從而將引用從快取中移除。不管你使用什麼管理快取的演算法,你實際上都在處理垃圾收集的工作,更簡單的辦法(除非你有特殊的需求,這也應該是最好的辦法)是讓垃圾收集器來處理,由它來決定回收哪個物件。
  當Java回收器遇到了弱引用的時候有可能會執行以下操作:
將WeakReference物件的referent域設定成為null,從而使該物件不再引用heap物件。
WeakReference引用過的記憶體堆上的物件一律被生命為finalizable。
當記憶體堆上的物件finalize()方法被執行而且該物件佔用的記憶體被釋放,WeakReference物件就會被新增到它的ReferenceQueue,前提條件是ReferenceQueue本身是存在的。
  4)清除:
  當引用物件的referent域設定為null,並且引用類在記憶體堆中引用的物件宣告為可結束的時候,該物件就可以清除,清除不做過多的講述
  5)虛可及物件(phantomly reachable):
  不是強可及物件,也不是軟可及物件,同樣不是弱可及物件,之所以把虛可及物件放到最後來講,主要也是因為它的特殊性,有時候我們又稱之為“幽靈物件”,已經結束的,可以通過虛引用來訪問該物件。我們使用類PhantomReference(java.lang.ref.PhantomReference)來訪問,這個類只能用於跟蹤被引用物件進行的收集,同樣的,可以用於執行per-mortern清除操作。PhantomReference必須與ReferenceQueue類一起使用。需要使用ReferenceQueue是因為它能夠充當通知機制,當垃圾收集器確定了某個物件是虛可及物件的時候,PhantomReference物件就被放在了它的ReferenceQueue上,這就是一個通知,表明PhantomReference引用的物件已經結束,可以收集了,一般情況下我們剛好在物件記憶體在回收之前採取該行為。這種引用不同於弱引用和軟引用,這種方式通過get()獲取到的物件總是返回null,僅僅當這些物件在ReferenceQueue佇列裡面的時候,我們可以知道它所引用的哪些對物件是死引用(Dead Reference)。而這種引用和弱引用的區別在於:
  弱引用(WeakReference)是在物件不可達的時候儘快進入ReferenceQueue佇列的,在finalization方法執行和垃圾回收之前是確實會發生的,理論上這類物件是不正確的物件,但是WeakReference物件可以繼續保持Dead狀態,
  虛引用(PhantomReference)是在物件確實已經從實體記憶體中移除過後才進入的ReferenceQueue佇列,而且get()方法會一直返回null
  當垃圾回收器遇到了虛引用的時候將有可能執行以下操作:
PhantomReference引用過的heap物件宣告為finalizable;
虛引用在堆物件釋放之前就新增到了它的ReferenceQueue裡面,這種情況使得我們可以在堆物件被回收之前採取操作【*:再次提醒,PhantomReference物件必須經過關聯的ReferenceQueue來建立,就是說必須和ReferenceQueue類配合操作】
  看似沒有用處的虛引用,有什麼用途呢?
首先,我們可以通過虛引用知道物件究竟什麼時候真正從記憶體裡面移除的,而且這也是唯一的途徑。
虛引用避過了finalize()方法,因為對於此方法的執行而言,虛引用真正引用到的物件是異常物件,若在該方法內要使用物件只能重建。一般情況垃圾回收器會輪詢兩次,一次標記為finalization,第二次進行真實的回收,而往往標記工作不能實時進行,或者垃圾回收其會等待一個物件去標記finalization。這種情況很有可能引起MemoryOut,而使用虛引用這種情況就會完全避免。因為虛引用在引用物件的過程不會去使得這個物件由Dead復活,而且這種物件是可以在回收週期進行回收的。
  在JVM內部,虛引用比起使用finalize()方法更加安全一點而且更加有效。而finaliaze()方法回收在虛擬機器裡面實現起來相對簡單,而且也可以處理大部分工作,所以我們仍然使用這種方式來進行物件回收的掃尾操作,但是有了虛引用過後我們可以選擇是否手動操作該物件使得程式更加高效完美。
  iv.防止記憶體洩漏[來自IBM開發中心]:
  1)使用軟引用阻止洩漏:
  [1]在Java語言中有一種形式的記憶體洩漏稱為物件遊離(Object Loitering):
  ——[$]物件遊離——
// 注意,這段程式碼屬於概念說明程式碼,實際應用中不要模仿
public class LeakyChecksum{
    private byte[] byteArray;