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外掛程式來查。 圖1 Eclipse的profiler外掛程式的Threads View 因為問題出在某個模組啟動之後,所以這時先把統計功能關閉可以使結果更有意義(如果全部採樣,初始化階段的資源消耗對結果影響很大),也可以加快程式啟動。等問題模組啟動後,開始啟動統計功能,此時點擊Threads View中某個線程,在Thread method View中就會顯示該線程中各調用的方法所啟動並執行時間。 圖2 點擊Threads View中的某個Thread,Thread methods View中就會顯示相關的內容 按Total time排序查看就可以找到哪些方法是佔用CPU最多的。 圖3 按Total time排序查看Thread methods的資料 但是當時我犯了非常嚴重的錯誤,以前在filter中把系統類別(例如java.*、sun.*等等)過濾了,忘了取消,結果查出來的結果非常莫名其妙,最佔用CPU的函數(達30%多)居然只有一個long型的指派陳述式!這是在profiler的屬性中設定的, 圖4 Eclipse的profiler外掛程式的運行屬性設定 後來想起有這個問題,把fliter重新設好,檢測結果就正常了,不過沒有什麼有價值的線索,佔用CPU較多的都是java系統包的方法(後來查出問題後回想才發現這裡其實是有線索的)。於是就懷疑是記憶體快用光時JVM的記憶體管理模組會大量佔用CPU,所以導致java程式的CPU佔有率偏高(現在還不知道是否確實如此),另一方面,這些偵查工具本身就大量消耗系統資源,也是導致CPU佔用為100%的主要原因。於是,又改為用Borland Optimizeit Suite檢查記憶體問題了。 先啟動Optimizeit Suite中的profiler模組: 圖5 啟動Profiler模組 圖6 在Profiler主介面中添加新Setting 注意使用Borland Optimizeit Suite時,jar中要把main class設好,classpath中要把所有用到的外部jar包全部輸入,否則無法啟動和正常運行。這個工具不太友好,無法啟動時輸出資訊很不明確,所以最好自己先檢查清楚了。 圖7 在Profiler的Setting中設定main class和classpath 一般測試時都有運行多次,當發現某些包跟問題無關時,可以把它們過濾掉,這樣最後得到的結果更容易閱讀。 圖8 設定filter 用Profiler啟動程式。運行到問題模組後,點擊工具列最右邊的按鈕,標記當時各對象佔用記憶體的情況。 圖9 標記當前記憶體情況(圖中紅柱上的黑線) 點擊工具列上像驚嘆號的按鈕,選擇顯示的內容。對這個問題,我覺得show size是最重要的。 圖10 設定顯示的內容 再運行一段時間後,按Size diff排序。發現佔用記憶體比較多的了後,雙擊該行,開啟該對象的詳細資料(目前bug已經改好,我只是隨便點一個類作為示範)。這時展開整棵樹,就可以看到該類所有執行個體分別是在什麼方法中產生的(如果跟蹤CPU資訊,也有這種資源分派樹顯示)。 圖11 某個類的所有對象的構造位置分布 因為佔用記憶體最多的往往是系統基礎資料型別 (Elementary Data Type),例如char,int[]等等,所以需要人工去判斷哪些類是真正的疑點。我發現佔用新申請記憶體排名第20-30之間有幾個類的對象數是完全相同的,而且持續同步增長,估計是某些線程中重複構造對象造成的,比其他以不規則速度增長的類更加可疑,於是就仔細檢查了這幾個的類的詳細資料,發現果然是來自同一個函數。再仔細看代碼,就找到記憶體流失的原因,原來是某行代碼寫錯了,不斷構造新的button。而儲存這些button是用Vector!說明我最早使用的檢查HashMap的思路是對的。可惜只是從自己的編程習慣出發,沒有考慮到Vector,否則問題就更容易發現了。 不過當時還曾經檢測到一個突變過程,至今沒有想明白原因。在沒有任何人對該電腦進行操作的情況下,CPU和記憶體佔有率在某個時刻同時暴增。 圖12 CPU佔用率突變 圖13 記憶體佔用突變 |