1. 程式人生 > >Java記憶體洩漏簡單的分析總結

Java記憶體洩漏簡單的分析總結

一、       理解Java記憶體回收機制 

Java的記憶體管理就是物件的分配和釋放問題。在Java中,記憶體的分配是由程式完成的,而記憶體的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程式設計師不需要通過呼叫函式來釋放記憶體,但它只能回收無用並且不再被其它物件引用的那些物件所佔用的空間。

不論哪種語言的記憶體分配方式,都需要返回所分配記憶體的真實地址,也就是返回一個指標到記憶體塊的首地址。Java中物件是採用new或者反射的方法建立的,這些物件的建立都是在堆(Heap)中分配的,所有物件的回收都是由Java虛擬機器通過垃圾回收機制完成的。GC為了能夠正確釋放物件,會監控每個物件的執行狀況,對他們的申請、引用、被引用、賦值等狀況進行監控,Java會使用有向圖的方法進行管理記憶體,實時監控物件是否可以達到,如果不可到達,則就將其回收,這樣也可以消除引用迴圈的問題。在Java語言中,判斷一個記憶體空間是否符合垃圾收集標準有兩個:一個是給物件賦予了空值null,以下再沒有呼叫過,另一個是給物件賦予了新值,這樣重新分配了記憶體空間。

在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。

Vectorv = new Vector(10);

for(int i = 1; i < 100; i++){

Objecto = 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中記憶體洩露的發生場景。具體主要有如下幾大類: 

1.靜態集合類引起記憶體洩露: 

像HashMap、Vector等的使用最容易出現記憶體洩露,這些靜態變數的生命週期和應用程式一致,他們所引用的所有的物件Object也不能被釋放,因為他們也將一直被Vector等引用著。 

2.當集合裡面的物件屬性被修改後,再呼叫remove()方法時不起作用。

3.監聽器 

在java 程式設計中,我們都需要和監聽器打交道,通常一個應用當中會用到很多監聽器,我們會呼叫一個控制元件的諸如addXXXListener()等方法來增加監聽器,但往往在釋放物件的時候卻沒有記住去刪除這些監聽器,從而增加了記憶體洩漏的機會。

4.各種連線 

比如資料庫連線(dataSourse.getConnection()),網路連線(socket)和io連線,除非其顯式的呼叫了其close()方法將其連線關閉,否則是不會自動被GC 回收的。對於Resultset 和Statement 物件可以不進行顯式回收,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收,而Connection一旦回收,Resultset 和Statement 物件就會立即為NULL。但是如果使用連線池,情況就不一樣了,除了要顯式地關閉連線,還必須顯式地關閉Resultset Statement 物件(關閉其中一個,另外一個也會關閉),否則就會造成大量的Statement 物件無法釋放,從而引起記憶體洩漏。這種情況下一般都會在try裡面去的連線,在finally裡面釋放連線。

5.內部類和外部模組等的引用 

內部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導致一系列的後繼類物件沒有釋放。此外程式設計師還要小心外部模組不經意的引用,例如程式設計師A 負責A 模組,呼叫了B 模組的一個方法如: 

publicvoid registerMsg(Object b); 

這種呼叫就要非常小心了,傳入了一個物件,很可能模組B就保持了對該物件的引用,這時候就需要注意模組B 是否提供相應的操作去除引用。

6.單例模式 

不正確使用單例模式是引起記憶體洩露的一個常見問題,單例物件在被初始化後將在JVM的整個生命週期中存在(以靜態變數的方式),如果單例物件持有外部物件的引用,那麼這個外部物件將不能被jvm正常回收,導致記憶體洩露

五、          如何檢測和處理記憶體洩漏

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

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

第二個步驟就是檢測Java的記憶體洩漏。在這裡我們通常使用一些工具來檢查Java程式的記憶體洩漏問題。市場上已有幾種專業檢查Java記憶體洩漏的工具,它們的基本工作原理大同小異,都是通過監測Java程式執行時,所有物件的申請、釋放等動作,將記憶體管理的所有資訊進行統計、分析、視覺化。開發人員將根據這些資訊判斷程式是否有記憶體洩漏問題。這些工具包括Optimizeit Profiler,JProbe Profiler,JinSight , Rational 公司的Purify等。

5.1檢測記憶體洩漏的存在

這裡我們將簡單介紹我們在使用Optimizeit檢查的過程。通常在知道發生記憶體洩漏之後,第一步是要弄清楚洩漏了什麼資料和哪個類的物件引起了洩漏。

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

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

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

5.2處理記憶體洩漏的方法

一旦知道確實發生了記憶體洩漏,就需要更專業的工具來查明為什麼會發生洩漏。JVM自己是不會告訴您的。這些專業工具從JVM獲得記憶體系統資訊的方法基本上有兩種:JVMTI和位元組碼技術(byte codeinstrumentation)。Java虛擬機器工具介面(Java Virtual MachineTools Interface,JVMTI)及其前身Java虛擬機器監視程式介面(Java Virtual MachineProfiling Interface,JVMPI)是外部工具與JVM通訊並從JVM收集資訊的標準化介面。位元組碼技術是指使用探測器處理位元組碼以獲得工具所需的資訊的技術。

Optimizeit是Borland公司的產品,主要用於協助對軟體系統進行程式碼優化和故障診斷,其中的Optimizeit Profiler主要用於記憶體洩漏的分析。Profiler的堆檢視就是用來觀察系統執行使用的記憶體大小和各個類的例項分配的個數的。

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

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

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

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