Java記憶體溢位的詳細解決方案

luxika發表於2012-02-17
http://java.e800.com.cn/articles/2007/413/1176400876794405339_1.html編者按:Java記憶體洩漏是每個Java程式設計師都會遇到的問題,程式在本地執行一切正常,可是佈署到遠端就會出現記憶體無限制的增長,最後系統癱瘓,那麼如何最快最好的檢測程式的穩定性,防止系統崩盤,作者用自已的親身經歷與各位網友分享解決這些問題的辦法。

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

一. Java是如何管理記憶體

為了判斷Java中是否有記憶體洩露,我們首先必須瞭解Java是如何管理記憶體的。Java的記憶體管理就是物件的分配和釋放問題。在Java中,記憶體的分配是由程式完成的,而記憶體的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程式設計師不需要通過呼叫函式來釋放記憶體,但它只能回收無用並且不再被其它物件引用的那些物件所佔用的空間。

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。

java 程式碼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 Profiler,JProbe Profiler,JinSight , Rational 公司的Purify等。

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

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

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

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


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

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

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

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

最後,進一步研究單個物件,看看它們是如何互相關聯的。藉助於Profiler工具,應用程式中的程式碼可以在分配時進行動態新增,以建立堆疊跟蹤。也有可以對系統中所有物件分配進行動態的堆疊跟蹤。這些堆疊跟蹤可以在工具中進行累積和分析。對每個被洩漏的例項物件,必然存在一條從某個牽引物件出發到達該物件的引用鏈。處於堆疊空間的牽引物件在被從棧中彈出後就失去其牽引的能力,變為非牽引物件。因此,在長時間的執行後,被洩露的物件基本上都是被作為類的靜態變數的牽引物件牽引。
總而言之, Java雖然有自動回收管理記憶體的功能,但記憶體洩漏也是不容忽視,它往往是破壞系統穩定性的重要因素。

相關文章