java垃圾回收和內存泄露的講解 – JAVA編程語言程序開發技術文章

1.垃圾收集算法的核心思想
Java語言建立瞭垃圾收集機制,用以跟蹤正在使用的對象和發現並回收不再使用(引用)的對象。該機制可以有效防范動態內存分配中可能發生的兩個危險:因內存垃圾過多而引發的內存耗盡,以及不恰當的內存釋放所造成的內存非法引用。
垃圾收集算法的核心思想是:對虛擬機可用內存空間,即堆空間中的對象進行識別,如果對象正在被引用,那麼稱其為存活對象,反之,如果對象不再被引用,則為垃圾對象,可以回收其占據的空間,用於再分配。垃圾收集算法的選擇和垃圾收集系統參數的合理調節直接影響著系統性能,因此需要開發人員做比較深入的瞭解。
 
2.觸發主GC(Garbage Collector)的條件
JVM進行次GC的頻率很高,但因為這種GC占用時間極短,所以對系統產生的影響不大。更值得關註的是主GC的觸發條件,因為它對系統影響很明顯。總的來說,有兩個條件會觸發主GC:
①當應用程序空閑時,即沒有應用線程在運行時,GC會被調用。因為GC在優先級最低的線程中進行,所以當應用忙時,GC線程就不會被調用,但以下條件除外。
②Java堆內存不足時,GC會被調用。當應用線程在運行,並在運行過程中創建新對象,若這時內存空間不足,JVM就會強制地調用GC線程,以便回收內存用於新的分配。若GC一次之後仍不能滿足內存分配的要求,JVM會再進行兩次GC作進一步的嘗試,若仍無法滿足要求,則  JVM將報“out of memory”的錯誤,Java應用將停止。
由於是否進行主GC由JVM根據系統環境決定,而系統環境在不斷的變化當中,所以主GC的運行具有不確定性,無法預計它何時必然出現,但可以確定的是對一個長期運行的應用來說,其主GC是反復進行的。
 
3.減少GC開銷的措施
根據上述GC的機制,程序的運行會直接影響系統環境的變化,從而影響GC的觸發。若不針對GC的特點進行設計和編碼,就會出現內存駐留等一系列負面影響。為瞭避免這些影響,基本的原則就是盡可能地減少垃圾和減少GC過程中的開銷。具體措施包括以下幾個方面:
(1)不要顯式調用System.gc()
此函數建議JVM進行主GC,雖然隻是建議而非一定,但很多情況下它會觸發主GC,從而增加主GC的頻率,也即增加瞭間歇性停頓的次數。
 
(2)盡量減少臨時對象的使用
臨時對象在跳出函數調用後,會成為垃圾,少用臨時變量就相當於減少瞭垃圾的產生,從而延長瞭出現上述第二個觸發條件出現的時間,減少瞭主GC的機會。
 
(3)對象不用時最好顯式置為Null
一般而言,為Null的對象都會被作為垃圾處理,所以將不用的對象顯式地設為Null,有利於GC收集器判定垃圾,從而提高瞭GC的效率。
 
(4)盡量使用StringBuffer,而不用String來累加字符串(詳見blog另一篇文章JAVA中String與StringBuffer)
由於String是固定長的字符串對象,累加String對象時,並非在一個String對象中擴增,而是重新創建新的String對象,如Str5=Str1+Str2+Str3+Str4,這條語句執行過程中會產生多個垃圾對象,因為對次作“+”操作時都必須創建新的String對象,但這些過渡對象對系統來說是沒有實際意義的,隻會增加更多的垃圾。避免這種情況可以改用StringBuffer來累加字符串,因StringBuffer是可變長的,它在原有基礎上進行擴增,不會產生中間對象。
 
(5)能用基本類型如Int,Long,就不用Integer,Long對象
基本類型變量占用的內存資源比相應對象占用的少得多,如果沒有必要,最好使用基本變量。
 
(6)盡量少用靜態對象變量
靜態變量屬於全局變量,不會被GC回收,它們會一直占用內存。
 
(7)分散對象創建或刪除的時間
集中在短時間內大量創建新對象,特別是大對象,會導致突然需要大量內存,JVM在面臨這種情況時,隻能進行主GC,以回收內存或整合內存碎片,從而增加主GC的頻率。集中刪除對象,道理也是一樣的。它使得突然出現瞭大量的垃圾對象,空閑空間必然減少,從而大大增加瞭下一次創建新對象時強制主GC的機會。
 
4.gc與finalize方法
⑴gc方法請求垃圾回收
使用System.gc()可以不管JVM使用的是哪一種垃圾回收的算法,都可以請求Java的垃圾回收。需要註意的是,調用System.gc()也僅僅是一個請求。JVM接受這個消息後,並不是立即做垃圾回收,而隻是對幾個垃圾回收算法做瞭加權,使垃圾回收操作容易發生,或提早發生,或回收較多而已。
 
⑵finalize方法透視垃圾收集器的運行
在JVM垃圾收集器收集一個對象之前 ,一般要求程序調用適當的方法釋放資源,但在沒有明確釋放資源的情況下,Java提供瞭缺省機制來終止化該對象釋放資源,這個方法就是finalize()。它的原型為:
protected void finalize() throws Throwable
在finalize()方法返回之後,對象消失,垃圾收集開始執行。原型中的throws Throwable表示它可以拋出任何類型的異常。
因此,當對象即將被銷毀時,有時需要做一些善後工作。可以把這些操作寫在finalize()方法裡。
protected void finalize()
{
// finalization code here
}
 
⑶代碼示例
 
class Garbage
{
    int index;
    static int count;
    Garbage()
    {
        count++;
        System.out.println("object "+count+" construct");
        setID(count);
    }
  
    void setID(int id)
    {
        index=id;
    }
  
    protected void finalize()  //重寫finalize方法
    {
        System.out.println("object "+index+" is reclaimed");
    }
  
    public static void main(String[] args)
    {
        new Garbage();
        new Garbage();
        new Garbage();
        new Garbage();
        System.gc();  //請求運行垃圾收集器
    }
}
 
5.Java 內存泄漏
由於采用瞭垃圾回收機制,任何不可達對象(對象不再被引用)都可以由垃圾收集線程回收。因此通常說的Java 內存泄漏其實是指無意識的、非故意的對象引用,或者無意識的對象保持。無意識的對象引用是指代碼的開發人員本來已經對對象使用完畢,卻因為編碼的錯誤而意外地保存瞭對該對象的引用(這個引用的存在並不是編碼人員的主觀意願),從而使得該對象一直無法被垃圾回收器回收掉,這種本來以為可以釋放掉的卻最終未能被釋放的空間可以認為是被“泄漏瞭”。
考慮下面的程序,在ObjStack類中,使用push和pop方法來管理堆棧中的對象。兩個方法中的索引(index)用於指示堆棧中下一個可用位置。push方法存儲對新對象的引用並增加索引值,而pop方法減小索引值並返回堆棧最上面的元素。在main方法中,創建瞭容量為64的棧,並64次調用push方法向它添加對象,此時index的值為64,隨後又32次調用pop方法,則index的值變為32,出棧意味著在堆棧中的空間應該被收集。但事實上,pop方法隻是減小瞭索引值,堆棧仍然保持著對那些對象的引用。故32個無用對象不會被GC回收,造成瞭內存滲漏。
 
public class ObjStack {
 private Object[] stack;
 private int index;
 ObjStack(int indexcount) {
  stack = new Object[indexcount];
  index = 0;
 }
 
 public void push(Object obj) {
  stack[index] = obj;
  index++;
  }
 public Object pop() {
  index–;
  return stack[index];
 }
}

public class Pushpop {
 public static void main(String[] args) {
  int i = 0;
  Object tempobj;
  ObjStack stack1 = new ObjStack(64);//new一個ObjStack對象,並調用有參構造函數。分配stack Obj數組的空間大小為64,可以存64個對象,從0開始存儲。
  while (i < 64)
  {
   tempobj = new Object();//循環new Obj對象,把每次循環的對象一一存放在stack Obj數組中。
   stack1.push(tempobj);
   i++;
   System.out.println("第" + i + "次進棧" + "/t");
  }
  while (i > 32)
  {
   tempobj = stack1.pop();//這裡造成瞭空間的浪費。
   //正確的pop方法可改成如下所指示,當引用被返回後,堆棧刪除對他們的引用,因此垃圾收集器在以後可以回收他們。
   /*
    * public Object pop() {index – -;Object temp = stack [index];stack [index]=null;return temp;}
    */
   i–;
   System.out.println("第" + (64 – i) + "次出棧" + "/t");
  }
 }
}

如何消除內存泄漏
  雖然Java虛擬機(JVM)及其垃圾收集器(garbage collector,GC)負責管理大多數的內存任務,Java軟件程序中還是有可能出現內存泄漏。實際上,這在大型項目中是一個常見的問題。避免內存泄漏的第一步是要弄清楚它是如何發生的。本文介紹瞭編寫Java代碼的一些常見的內存泄漏陷阱,以及編寫不泄漏代碼的一些最佳實踐。一旦發生瞭內存泄漏,要指出造成泄漏的代碼是非常困難的。因此本文還介紹瞭一種新工具,用來診斷泄漏並指出根本原因。該工具的開銷非常小,因此可以使用它來尋找處於生產中的系統的內存泄漏。
垃圾收集器的作用
  雖然垃圾收集器處理瞭大多數內存管理問題,從而使編程人員的生活變得更輕松瞭,但是編程人員還是可能犯錯而導致出現內存問題。簡單地說,GC循環地跟蹤所有來自“根”對象(堆棧對象、靜態對象、JNI句柄指向的對象,諸如此類)的引用,並將所有它所能到達的對象標記為活動的。程序隻可以操縱這些對象;其他的對象都被刪除瞭。因為GC使程序不可能到達已被刪除的對象,這麼做就是安全的。
  雖然內存管理可以說是自動化的,但是這並不能使編程人員免受思考內存管理問題之苦。例如,分配(以及釋放)內存總會有開銷,雖然這種開銷對編程人員來說是不可見的。創建瞭太多對象的程序將會比完成同樣的功能而創建的對象卻比較少的程序更慢一些(在其他條件相同的情況下)。
  而且,與本文更為密切相關的是,如果忘記“釋放”先前分配的內存,就可能造成內存泄漏。如果程序保留對永遠不再使用的對象的引用,這些對象將會占用並耗盡內存,這是因為自動化的垃圾收集器無法證明這些對象將不再使用。正如我們先前所說的,如果存在一個對對象的引用,對象就被定義為活動的,因此不能刪除。為瞭確保能回收對象占用的內存,編程人員必須確保該對象不能到達。這通常是通過將對象字段設置為null或者從集合(collection)中移除對象而完成的。但是,註意,當局部變量不再使用時,沒有必要將其顯式地設置為null。對這些變量的引用將隨著方法的退出而自動清除。
  概括地說,這就是內存托管語言中的內存泄漏產生的主要原因:保留下來卻永遠不再使用的對象引用。
典型泄漏
  既然我們知道瞭在Java中確實有可能發生內存泄漏,就讓我們來看一些典型的內存泄漏及其原因。
全局集合
  在大的應用程序中有某種全局的數據儲存庫是很常見的,例如一個JNDI樹或一個會話表。在這些情況下,必須註意管理儲存庫的大小。必須有某種機制從儲存庫中移除不再需要的數據。
  這可能有多種方法,但是最常見的一種是周期性運行的某種清除任務。該任務將驗證儲存庫中的數據,並移除任何不再需要的數據。
  另一種管理儲存庫的方法是使用反向鏈接(referrer)計數。然後集合負責統計集合中每個入口的反向鏈接的數目。這要求反向鏈接告訴集合何時會退出入口。當反向鏈接數目為零時,該元素就可以從集合中移除瞭。
緩存
  緩存是一種數據結構,用於快速查找已經執行的操作的結果。因此,如果一個操作執行起來很慢,對於常用的輸入數據,就可以將操作的結果緩存,並在下次調用該操作時使用緩存的數據。
  緩存通常都是以動態方式實現的,其中新的結果是在執行時添加到緩存中的。典型的算法是:
檢查結果是否在緩存中,如果在,就返回結果。
如果結果不在緩存中,就進行計算。
將計算出來的結果添加到緩存中,以便以後對該操作的調用可以使用。
  該算法的問題(或者說是潛在的內存泄漏)出在最後一步。如果調用該操作時有相當多的不同輸入,就將有相當多的結果存儲在緩存中。很明顯這不是正確的方法。
  為瞭預防這種具有潛在破壞性的設計,程序必須確保對於緩存所使用的內存容量有一個上限。因此,更好的算法是:
檢查結果是否在緩存中,如果在,就返回結果。
如果結果不在緩存中,就進行計算。
如果緩存所占的空間過大,就移除緩存最久的結果。
將計算出來的結果添加到緩存中,以便以後對該操作的調用可以使用。
  通過始終移除緩存最久的結果,我們實際上進行瞭這樣的假設:在將來,比起緩存最久的數據,最近輸入的數據更有可能用到。這通常是一個不錯的假設。
  新算法將確保緩存的容量處於預定義的內存范圍之內。確切的范圍可能很難計算,因為緩存中的對象在不斷變化,而且它們的引用包羅萬象。為緩存設置正確的大小是一項非常復雜的任務,需要將所使用的內存容量與檢索數據的速度加以平衡。
  解決這個問題的另一種方法是使用java.lang.ref.SoftReference類跟蹤緩存中的對象。這種方法保證這些引用能夠被移除,如果虛擬機的內存用盡而需要更多堆的話。
ClassLoader
  Java ClassLoader結構的使用為內存泄漏提供瞭許多可乘之機。正是該結構本身的復雜性使ClassLoader在內存泄漏方面存在如此多的問題。ClassLoader的特別之處在於它不僅涉及“常規”的對象引用,還涉及元對象引用,比如:字段、方法和類。這意味著隻要有對字段、方法、類或ClassLoader的對象的引用,ClassLoader就會駐留在JVM中。因為ClassLoader本身可以關聯許多類及其靜態字段,所以就有許多內存被泄漏瞭。
確定泄漏的位置
  通常發生內存泄漏的第一個跡象是:在應用程序中出現瞭OutOfMemoryError。這通常發生在您最不願意它發生的生產環境中,此時幾乎不能進行調試。有可能是因為測試環境運行應用程序的方式與生產系統不完全相同,因而導致泄漏隻出現在生產中。在這種情況下,需要使用一些開銷較低的工具來監控和查找內存泄漏。還需要能夠無需重啟系統或修改代碼就可以將這些工具連接到正在運行的系統上。可能最重要的是,當進行分析時,需要能夠斷開工具而保持系統不受幹擾。
  雖然OutOfMemoryError通常都是內存泄漏的信號,但是也有可能應用程序確實正在使用這麼多的內存;對於後者,或者必須增加JVM可用的堆的數量,或者對應用程序進行某種更改,使它使用較少的內存。但是,在許多情況下,OutOfMemoryError都是內存泄漏的信號。一種查明方法是不間斷地監控GC的活動,確定內存使用量是否隨著時間增加。如果確實如此,就可能發生瞭內存泄漏。

作者“安卓筆記”

發佈留言