JAVA內存泄漏分析(二)

Java內存泄漏分析
文章出處:http://hi.baidu.com/robin300/blog/item/8fcf4b347d13694e251f148e.html

Java語言相比C++的一個很大優勢就是Java可以自動管理內存的回收,這大大減少了程序員的負擔。然而,Java並不是杜絕了所有的內存問題,還是會有內存泄漏的問題,只不過原因和C++是不一樣的,所以出現得比較少。Java的內存垃圾回收機制是從程序的主要運行對象開始檢查引用鏈,當遍歷一遍後發現沒有被引用的孤立對象就作爲垃圾回收。詳細說明可以看ibm developerworks上的文章http://www-900.ibm.com/developerworks/cn/java/j-leaks/index_eng.shtml)。在現在代碼檢查工具越來越先進的情況下,C++的內存漏洞檢查已經變得容易很多,但Java的內存漏洞由於機制不一樣,反而無法通過工具直接檢查出來,只要靠輔助工具檢測,發現疑點,然後再手動解決。

下面是我調試一個大型軟件系統內存泄漏問題的過程,把其中失敗的做法也寫出來了,因爲都是有參考價值的:

現象:程序啓動後一直運行正常,但啓動某個模塊後,每分鐘多消耗1M的內存,持續增長到出現java.lang.OutOfMemoryError爲止,而CPU佔用率也是逐步上升至100%。

判斷:由於CPU和內存都出現問題,無法判斷哪個是主因,也可能是多個原因造成的,估計最可能是多線程或者內存泄漏的問題。

輔助工具:由於不是我自己寫的程序,所以沒法直接估計是代碼的哪一部分的問題,所以需要使用輔助工具,我比較常用的是Eclipse的插件ru.nlmk.eclipse.plugins.profiler(免費)和Borland公司的Borland Optimizeit Suite(收費)。因爲我是用Eclipse來開發程序,所以使用前者比較方便,個人感覺使用時佔用的系統資源少一點,而且在CPU檢查方面是略強一點,但是後者在內存檢查方面強大很多。

分析過程:當時由於CPU和內存都出現問題,無法判斷哪個是主因,需要檢測兩方面的數據。內存泄漏需要等程序運行一段時間後才能看出來,所以檢查很消耗時間。爲了減少干擾,我把系統中所有不是必要的各線程逐個關閉來試驗,然後把確認對內存問題沒有影響的線程都關閉。

首先考慮解決內存問題。因爲Java的內存泄漏問題往往跟HashMap相關,所以HashMap可能是一個突破口,而這麼大量的內存泄漏,很可能會是某些HashMap存放過多已經沒用的對象造成的(如果是這個原因,那改爲用WeakHashMap就可以解決問題了)。我從HashMap和LinkedHashMap派生了子類,其中加入了數據量檢查的信息,把系統中所有構造HashMap和LinkedHashMap的地方都改爲使用這些子類。但是運行後發現系統中構造的HashMap只有十幾個,而且每個指向的對象也不多,只有少數幾個是接近1千的,而仔細檢查代碼後,發現這幾個指向對象較多的HashMap也不會造成太大的內存泄漏問題。

這樣我的思路就中斷了,只好求助於工具來尋找疑點。因爲發現CPU佔用到100%一段時間後,程序就呈死機狀態沒法運行下去了,內存問題也就無從檢測,所以就決定先檢查CPU,用Eclipse的profiler插件來查。如圖:

http://download.smth.edu.cn/pcdownload.php?fid=6450

圖1 Eclipse的profiler插件的Threads View

因爲問題出在某個模塊啓動之後,所以這時先把統計功能關閉可以使結果更有意義(如果全部採樣,初始化階段的資源消耗對結果影響很大),也可以加快程序啓動。等問題模塊啓動後,開始啓動統計功能,此時點擊Threads View中某個線程,在Thread method View中就會顯示該線程中各調用的方法所運行的時間。

clip_image002_0064.jpg

圖2 點擊Threads View中的某個Thread,Thread methods View中就會顯示相關的內容

按Total time排序查看就可以找到哪些方法是佔用CPU最多的。

clip_image004_0002.jpg

圖3 按Total time排序查看Thread methods的數據

但是當時我犯了非常嚴重的錯誤,以前在filter中把系統類(例如java.*、sun.*等等)過濾了,忘了取消,結果查出來的結果非常莫名其妙,最佔用CPU的函數(達30%多)居然只有一個long型的賦值語句!這是在profiler的屬性中設置的,如圖:

clip_image005_0000.jpg

圖4 Eclipse的profiler插件的運行屬性設置

後來想起有這個問題,把fliter重新設好,檢測結果就正常了,不過沒有什麼有價值的線索,佔用CPU較多的都是java系統包的方法(後來查出問題後回想才發現這裏其實是有線索的)。於是就懷疑是內存快用光時JVM的內存管理模塊會大量佔用CPU,所以導致java程序的CPU佔有率偏高(現在還不知道是否確實如此),另一方面,這些檢測工具本身就大量消耗系統資源,也是導致CPU佔用爲100%的主要原因。於是,又改爲用Borland Optimizeit Suite檢查內存問題了。

先啓動Optimizeit Suite中的profiler模塊:

clip_image006_0002.jpg

圖5 啓動Profiler模塊

clip_image007_0000.jpg

圖6 在Profiler主界面中添加新Setting

注意使用Borland Optimizeit Suite時,jar中要把main class設好,classpath中要把所有用到的外部jar包全部輸入,否則無法啓動和正常運行。這個工具不太友好,無法啓動時輸出信息很不明確,所以最好自己先檢查清楚了。

clip_image008_0001.jpg

圖7 在Profiler的Setting中設置main class和classpath

一般測試時都有運行多次,當發現某些包跟問題無關時,可以把它們過濾掉,這樣最後得到的結果更容易閱讀。

clip_image009_0000.jpg

圖8 設置filter

用Profiler啓動程序。運行到問題模塊後,點擊工具欄最右邊的按鈕,標記當時各對象佔用內存的情況。

clip_image010_0002.jpg

圖9 標記當前內存情況(圖中紅柱上的黑線)

點擊工具欄上像感嘆號的按鈕,選擇顯示的內容。對這個問題,我覺得show size是最重要的。

http://download.smth.edu.cn/pcdownload.php?fid=6449

圖10 設置顯示的內容

再運行一段時間後,按Size diff排序。發現佔用內存比較多的了後,雙擊該行,打開該對象的詳細信息(目前bug已經改好,我只是隨便點一個類作爲示範)。這時展開整棵樹,就可以看到該類所有實例分別是在什麼方法中生成的(如果跟蹤CPU信息,也有這種資源分配樹顯示)。

clip_image012_0000.jpg

圖11 某個類的所有對象的構造位置分佈

因爲佔用內存最多的往往是系統基本數據類型,例如char,int[]等等,所以需要人工去判斷哪些類是真正的疑點。我發現佔用新申請內存排名第20-30之間有幾個類的對象數是完全相同的,而且持續同步增長,估計是某些線程中重複構造對象造成的,比其他以不規則速度增長的類更加可疑,於是就仔細檢查了這幾個的類的詳細信息,發現果然是來自同一個函數。再仔細看代碼,就找到內存泄漏的原因,原來是某行代碼寫錯了,不斷構造新的button。而保存這些button是用Vector!說明我最早使用的檢查HashMap的思路是對的。可惜只是從自己的編程習慣出發,沒有考慮到Vector,否則問題就更容易發現了。

不過當時還曾經檢測到一個突變過程,至今沒有想明白原因。在沒有任何人對該計算機進行操作的情況下,CPU和內存佔有率在某個時刻同時暴增。如圖:

clip_image013_0000.jpg

圖12 CPU佔用率突變

http://download.smth.edu.cn/pcdownload.php?fid=6457

圖13 內存佔用突變

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