JAVA記憶體流失分析(二)

來源:互聯網
上載者:User
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外掛程式來查。

圖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 記憶體佔用突變

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.