轉一篇有關Java的記憶體泄露的文章

來源:互聯網
上載者:User

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)中,基本都提供了這項技術,無需自己編程,但是,深入瞭解這項技術是非常必要的。
  資料庫連接池技術的思想非常簡單,將資料庫連接作為Object Storage Service在一個Vector對象中,一旦資料庫連接建立後,不同的資料庫訪問請求就可以共用這些串連,這樣,通過複用這些已經建立的資料庫連接,可以克服上述缺點,極大地節省系統資源和時間。
  資料庫連接池的主要操作如下:
  (1)建立資料庫連接池對象(伺服器啟動)。
  (2)按照事先指定的參數建立初始數量的資料庫連接(即:空閑串連數)。
  (3)對於一個資料庫訪問請求,直接從串連池中得到一個串連。如果資料庫連接池對象中沒有閒置串連,且串連數沒有達到最大(即:最大活躍串連數),建立一個新的資料庫連接。
  (4)存取資料庫。
  (5)關閉資料庫,釋放所有資料庫連接(此時的關閉資料庫連接,並非真正關閉,而是將其放入空閑隊列中。如實際空閑串連數大於初始空閑串連數則釋放串連)。
  (6)釋放資料庫連接池對象(伺服器停止、維護期間,釋放資料庫連接池對象,並釋放所有串連)。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.