轉一篇 有關JAVA的內存泄露的文章

 

轉一篇有關JAVA的內存泄露的文章(受益哦)
http://www.lybbs.net/news_read.do?newsPath=2007/9/25/1190684628458.html
 

1 引言
     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)釋放數據庫連接池對象(服務器停止、維護期間,釋放數據庫連接池對象,並釋放所有連接)。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章