1. 程式人生 > >轉一篇有關JAVA的記憶體洩露的文章

轉一篇有關JAVA的記憶體洩露的文章

引言
     Java的一個重要優點就是通過垃圾收集器GC (Garbage Collection)自動管理記憶體的回收,程式設計師不需要通過呼叫函式來釋放記憶體。因此,很多程式設計師認為Java 不存在記憶體洩漏問題,或者認為即使有記憶體洩漏也不是程式的責任,而是GC 或JVM的問題。其實,這種想法是不正確的,因為Java 也存在記憶體洩漏,但它的表現與C++不同。如果正在開發的Java 程式碼要全天24 小時在伺服器上執行,則記憶體漏洞在此處的影響就比在配置實用程式中的影響要大得多,即使最小的漏洞也會導致JVM耗盡全部可用記憶體。另外,在很多嵌入式系統中,記憶體的總量非常有限。在相反的情況下,即便程式的生存期較短,如果存在分配大量臨時物件(或者若干吞噬大量記憶體的物件)的任何Java 程式碼,而且當不再需要這些物件時也沒有取消對它們的引用,則仍然可能達到記憶體極限。


2 Java 記憶體回收機制
     Java 的記憶體管理就是物件的分配和釋放問題。分配記憶體的方式多種多樣,取決於該種語言的語法結構。但不論是哪一種語言的記憶體分配方式,最後都要返回所分配的記憶體塊的起始地址,即返回一個指標到記憶體塊的首地址。在Java 中所有物件都是在堆(Heap)
中分配的,物件的建立通常都是採用new或者是反射的方式,但物件釋放卻有直接的手段,所以物件的回收都是由Java虛擬機器通過垃圾收集器去完成的。這種收支兩條線的方法確實簡化了程式設計師的工作,但同時也加重了JVM的工作,這也是Java 程式執行速度較慢的原因之一。因為,GC 為了能夠正確釋放物件,GC 必須監控每一個物件的執行狀態,包括物件的申請、引用、被引用、賦值等,GC 都需要進行監控。監視物件狀態是為了更加準確地、及時地釋放物件,而釋放物件的根本原則就是該物件不再
被引用。Java 使用有向圖的方式進行記憶體管理,可以消除引用迴圈的問題,例如有三個物件,相互引用,只要它們和根程序不可達,那麼GC 也是可以回收它們的。在Java 語言中,判斷一塊記憶體空間是否符合垃圾收集器收集標準的標準只有兩個:一個是給物件賦予了空值null,以下再沒有呼叫過,另一個是給物件賦予了新值,即重新分配了記憶體空間。

3 Java 中的記憶體洩漏

3.1 Java 中記憶體洩漏與C++的區別
    在Java 中,記憶體洩漏就是存在一些被分配的物件,這些物件有下面兩個特點,首先,這些物件是可達的,即在有向圖中,存在通路可以與其相連;其次,這些物件是無用的,即程式以後不會再使用這些物件。如果物件滿足這兩個條件,這些物件就可以判定為Java 中的記憶體洩漏,這些物件不會被GC 所回收,然而它卻佔用記憶體。在C++中,記憶體洩漏的範圍更大一些。有些物件被分配了記憶體空間,然後卻不可達,由於C++中沒有GC,這些記憶體將永遠收
不回來。在Java 中,這些不可達的物件都由GC 負責回收,因此程式設計師不需要考慮這部分的記憶體洩漏。通過分析,可以得知,對於C++,程式設計師需要自己管理邊和頂點,而對於Java 程式設計師只需要管理邊就可以了(不需要管理頂點
的釋放)。通過這種方式,Java 提高了程式設計的效率。

3.2 記憶體洩漏示例
3.2.1 示例1
   在這個例子中,迴圈申請Object 物件,並將所申請的物件放入一個Vector 中,如果僅僅釋放引用本身,那麼Vector 仍然引用該物件,所以這個物件對GC 來說是不可回收的。因此,如果物件加入到Vector 後,還必須從Vector 中刪除,最簡單的方法就是將Vector物件設定為null。
Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{Object o = new Object();
v.add(o);
o = null;
}//

此時,所有的Object 物件都沒有被釋放,因為變數v 引用這些物件。實際上無用,而還被引用的物件,GC 就無能為力了(事實上GC 認為它還有用),這一點是導致記憶體洩漏最重要的原因。

(1)如果要釋放物件,就必須使其的引用記數為0,只有那些不再被引用的物件才能被釋放,這個原理很簡單,但是很重要,是導致記憶體洩漏的基本原因,也是解決記憶體洩漏方法的宗旨;
(2)程式設計師無須管理物件空間具體的分配和釋放過程,但必須要關注被釋放物件的引用記數是否為0;
(3)一個物件可能被其他物件引用的過程的幾種:
a.直接賦值,如上例中的A.a = E;
b.通過引數傳遞,例如public void addObject(Object E);
c.其它一些情況如系統呼叫等。


3.3 容易引起記憶體洩漏的幾大原因
3.3.1 靜態集合類
      像HashMap、Vector 
等靜態集合類的使用最容易引起記憶體洩漏,因為這些靜態變數的生命週期與應用程式一致,如示例1,如果該Vector 是靜態的,那麼它將一直存在,而其中所有的Object物件也不能被釋放,因為它們也將一直被該Vector 引用著。
3.3.2 監聽器
     在java 程式設計中,我們都需要和監聽器打交道,通常一個應用當中會用到很多監聽器,我們會呼叫一個控制元件的諸如addXXXListener()等方法來增加監聽器,但往往在釋放物件的時候卻沒有記住去刪除這些監聽器,從而增加了記憶體洩漏的機會。
3.3.3 物理連線
         一些物理連線,比如資料庫連線和網路連線,除非其顯式的關閉了連線,否則是不會自動被GC 回收的。Java 資料庫連線一般用DataSource.getConnection()來建立,當不再使用時必須用Close()方法來釋放,因為這些連線是獨立於JVM的。對於Resultset 和Statement 物件可以不進行顯式回收,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收,而Connection一旦回收,Resultset 和Statement 物件就會立即為NULL。但是如果使用連線池
,情況就不一樣了,除了要顯式地關閉連線,還必須顯式地關閉Resultset Statement 物件(關閉其中一個,另外一個也會關閉),否則就會造成大量的Statement 物件無法釋放,從而引起記憶體洩漏。


3.3.4 內部類和外部模組等的引用
        內部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導致一系列的後繼類物件沒有釋放。對於程式設計師而言,自己的程式很清楚,如果發現記憶體洩漏,自己對這些物件的引用可以很快定位並解決,但是現在的應用軟體
並非一個人實現,模組化的思想在現代軟體中非常明顯,所以程式設計師要小心外部模組不經意的引用,例如程式設計師A 負責A 模組,呼叫了B 模組的一個方法如:
public void registerMsg(Object b);
這種呼叫就要非常小心了,傳入了一個物件,很可能模組B就保持了對該物件的引用,這時候就需要注意模組B 是否提供相應的操作去除引用。


4 預防和檢測記憶體漏洞
    在瞭解了引起記憶體洩漏的一些原因後,應該儘可能地避免和發現記憶體洩漏。
(1)好的編碼習慣。最基本的建議就是儘早釋放無用物件的引用,大多數程式設計師在使用臨時變數的時候,都是讓引用變數在退出活動域後,自動設定為null。在使用這種方式時候,必須特別注意一些複雜的物件圖,例如陣列、列、樹、圖等,這些物件之間有相互引用關係較為複雜。對於這類物件,GC 回收它們一般效率較低。如果程式允許,儘早將不用的引用物件賦為null。另外建議幾點:
在確認一個物件無用後,將其所有引用顯式的置為null;
當類從Jpanel 或Jdialog 或其它容器類繼承的時候,刪除該物件之前不妨呼叫它的removeall()方法;在設一個引用變數為null 值之前,應注意該引用變數指向的物件是否被監聽,若有,要首先除去監聽器,然後才可以賦空值;當物件是一個Thread 的時候,刪除該物件之前不妨呼叫它的interrupt()方法;記憶體檢測過程中不僅要關注自己編寫的類物件,同時也要關注一些基本型別的物件,例如:int[]、String、char[]等等;如果有資料庫連線,使用try...finally 結構,在finally 中關閉Statement 物件和連線。
(2)好的測試工具。在開發中不能完全避免記憶體洩漏,關鍵要在發現有記憶體洩漏的時候能用好的測試工具迅速定位問題的所在。市場上已有幾種專業檢查Java 記憶體洩漏的工具,它們的基本工作原理大同小異,都是通過監測Java 程式執行時,所有物件的申請、釋放等動作,將記憶體管理的所有資訊進行統計、分析、視覺化。開發人員將根據這些資訊判斷程式是否有記憶體洩漏問題。這些工具包括Optimizeit Profiler、JProbe Profiler、JinSight、Rational 公司的Purify 等。 

記:
    映像(Reflector)是一個程式分析自己的能力。java.lang.reflect包提供了獲取關於欄位、建構函式、方法和類的修改器的資訊的能力。利用這些資訊可以建立和Java Beans元件打交道的工具。可以動態建立元件的特徵。
    堆(heap) :棧(stack)與堆(heap)都是Java用來在Ram中存放資料的地方。與C++不同,Java自動管理棧和堆,程式設計師不能直接地設定棧或堆。棧的優勢是,存取速度比堆要快,僅次於直接位於CPU中的暫存器。但缺點是,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。另外,棧資料可以共享,堆的優勢是可以動態地分配記憶體大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的資料。但缺點是,由於要在執行時動態分配記憶體,存取速度較慢。
    連線池:在實際應用開發中,特別是在WEB應用系統中,如果JSP、Servlet或EJB使用JDBC直接訪問資料庫中的資料,每一次資料訪問請求都必須經歷建立資料庫連線、開啟資料庫、存取資料和關閉資料庫連線等步驟,而連線並開啟資料庫是一件既消耗資源又費時的工作,如果頻繁發生這種資料庫操作,系統的效能必然會急劇下降,甚至會導致系統崩潰。資料庫連線池技術是解決這個問題最常用的方法,在許多應用程式伺服器(例如:Weblogic,WebSphere,JBoss)中,基本都提供了這項技術,無需自己程式設計,但是,深入瞭解這項技術是非常必要的。
  資料庫連線池技術的思想非常簡單,將資料庫連線作為物件儲存在一個Vector物件中,一旦資料庫連線建立後,不同的資料庫訪問請求就可以共享這些連線,這樣,通過複用這些已經建立的資料庫連線,可以克服上述缺點,極大地節省系統資源和時間。
  資料庫連線池的主要操作如下:
  (1)建立資料庫連線池物件(伺服器啟動)。
  (2)按照事先指定的引數建立初始數量的資料庫連線(即:空閒連線數)。
  (3)對於一個數據庫訪問請求,直接從連線池中得到一個連線。如果資料庫連線池物件中沒有空閒的連線,且連線數沒有達到最大(即:最大活躍連線數),建立一個新的資料庫連線。
  (4)存取資料庫。
  (5)關閉資料庫,釋放所有資料庫連線(此時的關閉資料庫連線,並非真正關閉,而是將其放入空閒佇列中。如實際空閒連線數大於初始空閒連線數則釋放連線)。
  (6)釋放資料庫連線池物件(伺服器停止、維護期間,釋放資料庫連線池物件,並釋放所有連線)。