java內存泄露

http://tech.it168.com/j/n/2007-04-12/200704121248421_4.shtml

 

 

編者按:Java內存泄漏是每個Java程序員都會遇到的問題,程序在本地運行一切正常,可是佈署到遠端就會出現內存無限制的增長,最後系統癱瘓,那麼如何最快最好的檢測程序的穩定性,防止系統崩盤,作者用自已的親身經歷與各位網友分享解決這些問題的辦法。

   
作爲Internet最流行的編程語言之一,Java現正非常流行。我們的網絡應用程序就主要採用Java語言開發,大體上分爲客戶端、服務器和數據庫三個層次。在進入測試過程中,我們發現有一個程序模塊系統內存和CPU資源消耗急劇增加,持續增長到出現java.lang.OutOfMemoryError爲止。經過分析Java內存泄漏是破壞系統的主要因素。這裏與大家分享我們在開發過程中遇到的Java內存泄漏的檢測和處理解決過程
.

   
. Java是如何管理內存


   
爲了判斷Java中是否有內存泄露,我們首先必須瞭解Java是如何管理內存的。Java的內存管理就是對象的分配和釋放問題。在Java中,內存的分配是由程序完成的,而內存的釋放是由垃圾收集器(Garbage CollectionGC)完成的,程序員不需要通過調用函數來釋放內存,但它只能回收無用並且不再被其它對象引用的那些對象所佔用的空間。


    Java
的內存垃圾回收機制是從程序的主要運行對象開始檢查引用鏈,當遍歷一遍後發現沒有被引用的孤立對象就作爲垃圾回收。GC爲了能夠正確釋放對象,必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC都需要進行監控。監視對象狀態是爲了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。


   
Java中,這些無用的對象都由GC負責回收,因此程序員不需要考慮這部分的內存泄露。雖然,我們有幾個函數可以訪問GC,例如運行GC的函數System.gc(),但是根據Java語言規範定義,該函數不保證JVM的垃圾收集器一定會執行。因爲不同的JVM實現者可能使用不同的算法管理GC。通常GC的線程的優先級別較低。JVM調用GC的策略也有很多種,有的是內存使用到達一定程度時,GC纔開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。

. 什麼是Java中的內存泄露

   
導致內存泄漏主要的原因是,先前申請了內存空間而忘記了釋放。如果程序中存在對無用對象的引用,那麼這些對象就會駐留內存,消耗內存,因爲無法讓垃圾回收器GC驗證這些對象是否不再需要。如果存在對象的引用,這個對象就被定義爲"有效的活動",同時不會被釋放。要確定對象所佔內存將被回收,我們就要務必確認該對象不再會被使用。典型的做法就是把對象數據成員設爲null或者從集合中移除該對象。但當局部變量不需要時,不需明顯的設爲null,因爲一個方法執行完畢時,這些引用會自動被清理。

   
Java中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是有被引用的,即在有向樹形圖中,存在樹枝通路可以與其相連;其次,這些對象是無用的,即程序以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定爲Java中的內存泄漏,這些對象不會被GC所回收,然而它卻佔用內存。


   
這裏引用一個常看到的例子,在下面的代碼中,循環申請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認爲它還有用),這一點是導致內存泄漏最重要的原因。 再引用另一個例子來說明Java的內存泄漏。假設有一個日誌類Logger,其提供一個靜態的log(String msg),任何其它類都可以調用Logger.Log(message)來將message的內容記錄到系統的日誌文件中。

    Logger
類有一個類型爲HashMap的靜態變量temp,每次在執行log(message)的時候,都首先將message的值寫入temp(以當前線程+當前時間爲鍵),在退出之前再從temp中將以當前線程和當前時間爲鍵的條目刪除。注意,這裏當前時間是不斷變化的,所以log在退出之前執行刪除條目的操作並不能刪除執行之初寫入的條目。這樣,任何一個作爲參數傳給log的字符串最終由於被Logger的靜態變量temp引用,而無法得到回收,這種對象保持就是我們所說的Java內存泄漏。總的來說,內存管理中的內存泄漏產生的主要原因:保留下來卻永遠不再使用的對象引用。

. 幾種典型的內存泄漏

   
我們知道了在Java中確實會存在內存泄漏,那麼就讓我們看一看幾種典型的泄漏,並找出他們發生的原因和解決方法。

    3.1
全局集合

   
在大型應用程序中存在各種各樣的全局數據倉庫是很普遍的,比如一個JNDI-tree或者一個session table。在這些情況下,必須注意管理儲存庫的大小。必須有某種機制從儲存庫中移除不再需要的數據。


   
通常有很多不同的解決形式,其中最常用的是一種週期運行的清除作業。這個作業會驗證倉庫中的數據然後清除一切不需要的數據。


   
另一種管理儲存庫的方法是使用反向鏈接(referrer)計數。然後集合負責統計集合中每個入口的反向鏈接的數目。這要求反向鏈接告訴集合何時會退出入口。當反向鏈接數目爲零時,該元素就可以從集合中移除了。

  

    3.2
緩存

   
緩存一種用來快速查找已經執行過的操作結果的數據結構。因此,如果一個操作執行需要比較多的資源並會多次被使用,通常做法是把常用的輸入數據的操作結果進行緩存,以便在下次調用該操作時使用緩存的數據。緩存通常都是以動態方式實現的,如果緩存設置不正確而大量使用緩存的話則會出現內存溢出的後果,因此需要將所使用的內存容量與檢索數據的速度加以平衡。


   
常用的解決途徑是使用java.lang.ref.SoftReference類堅持將對象放入緩存。這個方法可以保證當虛擬機用完內存或者需要更多堆的時候,可以釋放這些對象的引用。


    3.3
類裝載器

    Java
類裝載器的使用爲內存泄漏提供了許多可乘之機。一般來說類裝載器都具有複雜結構,因爲類裝載器不僅僅是隻與"常規"對象引用有關,同時也和對象內部的引用有關。比如數據變量,方法和各種類。這意味着只要存在對數據變量,方法,各種類和對象的類裝載器,那麼類裝載器將駐留在JVM中。既然類裝載器可以同很多的類關聯,同時也可以和靜態數據變量關聯,那麼相當多的內存就可能發生泄漏。 

 


   
. 如何檢測和處理內存泄漏
   
如何查找引起內存泄漏的原因一般有兩個步驟:第一是安排有經驗的編程人員對代碼進行走查和分析,找出內存泄漏發生的位置;第二是使用專門的內存泄漏測試工具進行測試。

   
第一個步驟在代碼走查的工作中,可以安排對系統業務和開發語言工具比較熟悉的開發人員對應用的代碼進行了交叉走查,儘量找出代碼中存在的數據庫連接聲明和結果集未關閉、代碼冗餘等故障代碼。


   
第二個步驟就是檢測Java的內存泄漏。在這裏我們通常使用一些工具來檢查Java程序的內存泄漏問題。市場上已有幾種專業檢查Java內存泄漏的工具,它們的基本工作原理大同小異,都是通過監測Java程序運行時,所有對象的申請、釋放等動作,將內存管理的所有信息進行統計、分析、可視化。開發人員將根據這些信息判斷程序是否有內存泄漏問題。這些工具包括Optimizeit ProfilerJProbe ProfilerJinSight , Rational 公司的Purify等。


    4.1
檢測內存泄漏的存在

   
這裏我們將簡單介紹我們在使用Optimizeit檢查的過程。通常在知道發生內存泄漏之後,第一步是要弄清楚泄漏了什麼數據和哪個類的對象引起了泄漏。

   
一般說來,一個正常的系統在其運行穩定後其內存的佔用量是基本穩定的,不應該是無限制的增長的。同樣,對任何一個類的對象的使用個數也有一個相對穩定的上限,不應該是持續增長的。根據這樣的基本假設,我們持續地觀察系統運行時使用的內存的大小和各實例的個數,如果內存的大小持續地增長,則說明系統存在內存泄漏,如果特定類的實例對象個數隨時間而增長(就是所謂的“增長率”),則說明這個類的實例可能存在泄漏情況。


   
另一方面通常發生內存泄漏的第一個跡象是:在應用程序中出現了OutOfMemoryError。在這種情況下,需要使用一些開銷較低的工具來監控和查找內存泄漏。雖然OutOfMemoryError也有可能應用程序確實正在使用這麼多的內存;對於這種情況則可以增加JVM可用的堆的數量,或者對應用程序進行某種更改,使它使用較少的內存。


   
但是,在許多情況下,OutOfMemoryError都是內存泄漏的信號。一種查明方法是不間斷地監控GC的活動,確定內存使用量是否隨着時間增加。如果確實如此,就可能發生了內存泄漏。
1

 

4.2處理內存泄漏的方法
   
一旦知道確實發生了內存泄漏,就需要更專業的工具來查明爲什麼會發生泄漏。JVM自己是不會告訴您的。這些專業工具從JVM獲得內存系統信息的方法基本上有兩種:JVMTI和字節碼技術(byte code instrumentation)Java虛擬機工具接口(Java Virtual Machine Tools InterfaceJVMTI)及其前身Java虛擬機監視程序接口(Java Virtual Machine Profiling InterfaceJVMPI)是外部工具與JVM通信並從JVM收集信息的標準化接口。字節碼技術是指使用探測器處理字節碼以獲得工具所需的信息的技術。

    Optimizeit
Borland公司的產品,主要用於協助對軟件系統進行代碼優化和故障診斷,其中的Optimizeit Profiler主要用於內存泄漏的分析。Profiler的堆視圖就是用來觀察系統運行使用的內存大小和各個類的實例分配的個數的。


   
首先,Profiler會進行趨勢分析,找出是哪個類的對象在泄漏。系統運行長時間後可以得到四個內存快照。對這四個內存快照進行綜合分析,如果每一次快照的內存使用都比上一次有增長,可以認定系統存在內存泄漏,找出在四個快照中實例個數都保持增長的類,這些類可以初步被認定爲存在泄漏。通過數據收集和初步分析,可以得出初步結論:系統是否存在內存泄漏和哪些對象存在泄漏(被泄漏)


   
接下來,看看有哪些其他的類與泄漏的類的對象相關聯。前面已經談到Java中的內存泄漏就是無用的對象保持,簡單地說就是因爲編碼的錯誤導致了一條本來不應該存在的引用鏈的存在(從而導致了被引用的對象無法釋放),因此內存泄漏分析的任務就是找出這條多餘的引用鏈,並找到其形成的原因。查看對象分配到哪裏是很有用的。同時只知道它們如何與其他對象相關聯(即哪些對象引用了它們)是不夠的,關於它們在何處創建的信息也很有用。


   
最後,進一步研究單個對象,看看它們是如何互相關聯的。藉助於Profiler工具,應用程序中的代碼可以在分配時進行動態添加,以創建堆棧跟蹤。也有可以對系統中所有對象分配進行動態的堆棧跟蹤。這些堆棧跟蹤可以在工具中進行累積和分析。對每個被泄漏的實例對象,必然存在一條從某個牽引對象出發到達該對象的引用鏈。處於堆棧空間的牽引對象在被從棧中彈出後就失去其牽引的能力,變爲非牽引對象。因此,在長時間的運行後,被泄露的對象基本上都是被作爲類的靜態變量的牽引對象牽引。

   
總而言之, Java雖然有自動回收管理內存的功能,但內存泄漏也是不容忽視,它往往是破壞系統穩定性的重要因素。

1

. 什麼是Java中的內存泄露

   
導致內存泄漏主要的原因是,先前申請了內存空間而忘記了釋放。如果程序中存在對無用對象的引用,那麼這些對象就會駐留內存,消耗內存,因爲無法讓垃圾回收器GC驗證這些對象是否不再需要。如果存在對象的引用,這個對象就被定義爲"有效的活動",同時不會被釋放。要確定對象所佔內存將被回收,我們就要務必確認該對象不再會被使用。典型的做法就是把對象數據成員設爲null或者從集合中移除該對象。但當局部變量不需要時,不需明顯的設爲null,因爲一個方法執行完畢時,這些引用會自動被清理。

   
Java中,內存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是有被引用的,即在有向樹形圖中,存在樹枝通路可以與其相連;其次,這些對象是無用的,即程序以後不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定爲Java中的內存泄漏,這些對象不會被GC所回收,然而它卻佔用內存。


   
這裏引用一個常看到的例子,在下面的代碼中,循環申請Object對象,並將所申請的對象放入一個Vector中,如果僅僅釋放對象本身,但因爲Vector仍然引用該對象,所以這個對象對GC來說是不可回收的。因此,如果對象加入到Vector後,還必須從Vector中刪除,最簡單的方法就是將Vector對象設置爲null

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)

http://www.CodeHighlighter.com/

-->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認爲它還有用),這一點是導致內存泄漏最重要的原因。 再引用另一個例子來說明Java的內存泄漏。假設有一個日誌類Logger,其提供一個靜態的log(String msg),任何其它類都可以調用Logger.Log(message)來將message的內容記錄到系統的日誌文件中。

    Logger
類有一個類型爲HashMap的靜態變量temp,每次在執行log(message)的時候,都首先將message的值寫入temp(以當前線程+當前時間爲鍵),在退出之前再從temp中將以當前線程和當前時間爲鍵的條目刪除。注意,這裏當前時間是不斷變化的,所以log在退出之前執行刪除條目的操作並不能刪除執行之初寫入的條目。這樣,任何一個作爲參數傳給log的字符串最終由於被Logger的靜態變量temp引用,而無法得到回收,這種對象保持就是我們所說的Java內存泄漏。總的來說,內存管理中的內存泄漏產生的主要原因:保留下來卻永遠不再使用的對象引用。

 

 . 幾種典型的內存泄漏

   
我們知道了在Java中確實會存在內存泄漏,那麼就讓我們看一看幾種典型的泄漏,並找出他們發生的原因和解決方法。

    3.1
全局集合

   
在大型應用程序中存在各種各樣的全局數據倉庫是很普遍的,比如一個JNDI-tree或者一個session table。在這些情況下,必須注意管理儲存庫的大小。必須有某種機制從儲存庫中移除不再需要的數據。


   
通常有很多不同的解決形式,其中最常用的是一種週期運行的清除作業。這個作業會驗證倉庫中的數據然後清除一切不需要的數據。


   
另一種管理儲存庫的方法是使用反向鏈接(referrer)計數。然後集合負責統計集合中每個入口的反向鏈接的數目。這要求反向鏈接告訴集合何時會退出入口。當反向鏈接數目爲零時,該元素就可以從集合中移除了。

  

    3.2
緩存

   
緩存一種用來快速查找已經執行過的操作結果的數據結構。因此,如果一個操作執行需要比較多的資源並會多次被使用,通常做法是把常用的輸入數據的操作結果進行緩存,以便在下次調用該操作時使用緩存的數據。緩存通常都是以動態方式實現的,如果緩存設置不正確而大量使用緩存的話則會出現內存溢出的後果,因此需要將所使用的內存容量與檢索數據的速度加以平衡。


   
常用的解決途徑是使用java.lang.ref.SoftReference類堅持將對象放入緩存。這個方法可以保證當虛擬機用完內存或者需要更多堆的時候,可以釋放這些對象的引用。


    3.3
類裝載器

    Java
類裝載器的使用爲內存泄漏提供了許多可乘之機。一般來說類裝載器都具有複雜結構,因爲類裝載器不僅僅是隻與"常規"對象引用有關,同時也和對象內部的引用有關。比如數據變量,方法和各種類。這意味着只要存在對數據變量,方法,各種類和對象的類裝載器,那麼類裝載器將駐留在JVM中。既然類裝載器可以同很多的類關聯,同時也可以和靜態數據變量關聯,那麼相當多的內存就可能發生泄漏。
 
. 如何檢測和處理內存泄漏

   
如何查找引起內存泄漏的原因一般有兩個步驟:第一是安排有經驗的編程人員對代碼進行走查和分析,找出內存泄漏發生的位置;第二是使用專門的內存泄漏測試工具進行測試。


   
第一個步驟在代碼走查的工作中,可以安排對系統業務和開發語言工具比較熟悉的開發人員對應用的代碼進行了交叉走查,儘量找出代碼中存在的數據庫連接聲明和結果集未關閉、代碼冗餘等故障代碼。


   
第二個步驟就是檢測Java的內存泄漏。在這裏我們通常使用一些工具來檢查Java程序的內存泄漏問題。市場上已有幾種專業檢查Java內存泄漏的工具,它們的基本工作原理大同小異,都是通過監測Java程序運行時,所有對象的申請、釋放等動作,將內存管理的所有信息進行統計、分析、可視化。開發人員將根據這些信息判斷程序是否有內存泄漏問題。這些工具包括Optimizeit ProfilerJProbe ProfilerJinSight , Rational 公司的Purify等。


    4.1
檢測內存泄漏的存在

   
這裏我們將簡單介紹我們在使用Optimizeit檢查的過程。通常在知道發生內存泄漏之後,第一步是要弄清楚泄漏了什麼數據和哪個類的對象引起了泄漏。

   
一般說來,一個正常的系統在其運行穩定後其內存的佔用量是基本穩定的,不應該是無限制的增長的。同樣,對任何一個類的對象的使用個數也有一個相對穩定的上限,不應該是持續增長的。根據這樣的基本假設,我們持續地觀察系統運行時使用的內存的大小和各實例的個數,如果內存的大小持續地增長,則說明系統存在內存泄漏,如果特定類的實例對象個數隨時間而增長(就是所謂的增長率),則說明這個類的實例可能存在泄漏情況。


   
另一方面通常發生內存泄漏的第一個跡象是:在應用程序中出現了OutOfMemoryError。在這種情況下,需要使用一些開銷較低的工具來監控和查找內存泄漏。雖然OutOfMemoryError也有可能應用程序確實正在使用這麼多的內存;對於這種情況則可以增加JVM可用的堆的數量,或者對應用程序進行某種更改,使它使用較少的內存。


   
但是,在許多情況下,OutOfMemoryError都是內存泄漏的信號。一種查明方法是不間斷地監控GC的活動,確定內存使用量是否隨着時間增加。如果確實如此,就可能發生了內存泄漏。

14.2處理內存泄漏的方法
   
一旦知道確實發生了內存泄漏,就需要更專業的工具來查明爲什麼會發生泄漏。JVM自己是不會告訴您的。這些專業工具從JVM獲得內存系統信息的方法基本上有兩種:JVMTI和字節碼技術(byte code instrumentation)Java虛擬機工具接口(Java Virtual Machine Tools InterfaceJVMTI)及其前身Java虛擬機監視程序接口(Java Virtual Machine Profiling InterfaceJVMPI)是外部工具與JVM通信並從JVM收集信息的標準化接口。字節碼技術是指使用探測器處理字節碼以獲得工具所需的信息的技術。

    Optimizeit
Borland公司的產品,主要用於協助對軟件系統進行代碼優化和故障診斷,其中的Optimizeit Profiler主要用於內存泄漏的分析。Profiler的堆視圖就是用來觀察系統運行使用的內存大小和各個類的實例分配的個數的。


   
首先,Profiler會進行趨勢分析,找出是哪個類的對象在泄漏。系統運行長時間後可以得到四個內存快照。對這四個內存快照進行綜合分析,如果每一次快照的內存使用都比上一次有增長,可以認定系統存在內存泄漏,找出在四個快照中實例個數都保持增長的類,這些類可以初步被認定爲存在泄漏。通過數據收集和初步分析,可以得出初步結論:系統是否存在內存泄漏和哪些對象存在泄漏(被泄漏)


   
接下來,看看有哪些其他的類與泄漏的類的對象相關聯。前面已經談到Java中的內存泄漏就是無用的對象保持,簡單地說就是因爲編碼的錯誤導致了一條本來不應該存在的引用鏈的存在(從而導致了被引用的對象無法釋放),因此內存泄漏分析的任務就是找出這條多餘的引用鏈,並找到其形成的原因。查看對象分配到哪裏是很有用的。同時只知道它們如何與其他對象相關聯(即哪些對象引用了它們)是不夠的,關於它們在何處創建的信息也很有用。


   
最後,進一步研究單個對象,看看它們是如何互相關聯的。藉助於Profiler工具,應用程序中的代碼可以在分配時進行動態添加,以創建堆棧跟蹤。也有可以對系統中所有對象分配進行動態的堆棧跟蹤。這些堆棧跟蹤可以在工具中進行累積和分析。對每個被泄漏的實例對象,必然存在一條從某個牽引對象出發到達該對象的引用鏈。處於堆棧空間的牽引對象在被從棧中彈出後就失去其牽引的能力,變爲非牽引對象。因此,在長時間的運行後,被泄露的對象基本上都是被作爲類的靜態變量的牽引對象牽引。

   
總而言之, Java雖然有自動回收管理內存的功能,但內存泄漏也是不容忽視,它往往是破壞系統穩定性的重要因素。

1

 

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