根據(jù)個人的開發(fā)和系統(tǒng)調(diào)優(yōu)經(jīng)驗,大部分的內(nèi)存泄露都和不好的開發(fā)習(xí)慣有直接關(guān)系,有幾個開發(fā)經(jīng)驗可以有效預(yù)防OOM,總結(jié)下貼出來和大家分享。
一、批量和分頁
老生常談的話題,簡單,但是非常實用。
每個合格的coder對數(shù)據(jù)的處理,必須要有分頁或批量多次的意識。大數(shù)據(jù)量的讀取或查詢結(jié)果集是內(nèi)存占用大戶,是系統(tǒng)性能下降的直接原因之一。
在典型的互聯(lián)網(wǎng)web應(yīng)用中,數(shù)據(jù)量較大且高并發(fā)的情況下,不分頁,或者不進行批量處理,每次總是取出很多用戶數(shù)據(jù),很容易造成內(nèi)存開銷過大,系統(tǒng)內(nèi)存吃緊。再比如我們有時候進行文件操作,讀取文件內(nèi)容的時候就要斟酌考慮文件有多大。
如果你的項目中還在出現(xiàn)不分青紅皂白一次查詢返回N(N有多大?)條記錄的DataSet、DataTable或者列表記錄等等情況,或者查詢大量數(shù)據(jù)寫入臨時表,或者一次讀取很大文件內(nèi)容......呵呵。
二、慎用靜態(tài)
這個也是常見但是比較隱式的引起內(nèi)存泄露的元兇之一。
靜態(tài)類、靜態(tài)字段、靜態(tài)屬性,靜態(tài)委托,靜態(tài)方法…靜態(tài)的好處當(dāng)然顯而易見,比如調(diào)用方便,常駐內(nèi)存提高性能等等,所以,有些代碼索性靜態(tài)到底,除了實體層,在表現(xiàn)層(說起來非?膳,我曾經(jīng)在web應(yīng)用程序中看到竟然有人名目張膽地大肆使用靜態(tài)字段),數(shù)據(jù)訪問層、業(yè)務(wù)邏輯層、公共組件、配置管理等等等等,能靜態(tài)的全給它用上靜態(tài)。
比起大數(shù)據(jù)查詢造成的常發(fā)性的內(nèi)存不足,使用靜態(tài)太多的應(yīng)用程序一時半會也不會內(nèi)存泄露?呻S著系統(tǒng)的運行,靜態(tài)的東西越來越多,內(nèi)存開銷也就越大,由于GC的回收策略,無效的靜態(tài)所占內(nèi)存又不容易及時釋放,久而久之就造成了內(nèi)存不足。
使用靜態(tài)的情況在分層應(yīng)用程序中非常常見,而且由于它的好處容易得到體現(xiàn)而隱藏的風(fēng)險不容易暴露出來,所以很多程序員還是非常執(zhí)著地使用靜態(tài)。
三、二方庫、三方庫,非托管資源,優(yōu)先使用Dispose模式
有些應(yīng)用程序需要借助包裝的二方庫或者三方庫,或者使用了非托管資源,如com組件等等,由于.NET自動內(nèi)存管理和回收,很多人覺得我用一下完成功能就Over了。
實際情況是你調(diào)用了別人的庫,別人的庫也很可能當(dāng)仁不讓地占用了你的內(nèi)存而你還不自知。
每次調(diào)用別人的資源都應(yīng)該有個警覺性:用你的類庫可以,占用我的(內(nèi)存)不行。
如果你熟悉自動內(nèi)存管理,熟悉GC,理解Dispose模式,那么一定會在調(diào)用別人的資源的時候想著還是using一下為妙,或者,強制賦個null也是舉手之勞,要相信某些良好的編程習(xí)慣可以讓自動內(nèi)存管理更有效。怕的就是很多人拿來主義,測試不充分,自己調(diào)用成功功能完成開發(fā)就OK了,交接給別人自己走人。
四、減少字符串臨時對象
這個實在是太熟悉不過了,不論是什么形式的應(yīng)用程序,哪里能少得了字符串的身影?
看到它們有人條件反射似地想到拼接字符串,想到駐留池等等等等。
沒錯,不合理地使用String進行操作也會造成內(nèi)存不足異常,而且這絕不是聳人聽聞。
舉例來說,對于String的+=,很多應(yīng)用程序中這個操作層出不窮。我們都知道+=操作會造成很多臨時字符串對象,這些對象由于CLR對字符串的駐留處理,容易占用內(nèi)存空間。如果是高并發(fā)的web應(yīng)用程序,而字符串操作隨處可見,且字符串的長度又不確定地長,前端頁面各種各樣的拼接,久而久之,內(nèi)存占用就會是一個重大問題。CLR對字符串的優(yōu)化處理使得字符串不被優(yōu)先回收,如果字符串操作頻繁,臨時字符串較長(比如大于等于85000字節(jié))而出現(xiàn)大對象堆的分配,那么更容易出現(xiàn)內(nèi)存泄露。
很多人可能都會想到如何優(yōu)化程序去降低string的臨時對象的生成概率。對的,StringBuilder的出現(xiàn)就順理成章了。
五、其他經(jīng)驗
1、Session的不當(dāng)使用,尤其是使用InProc模式的會話,為了保持狀態(tài)而選擇使用Session,如用戶訪問量較大將極大消耗服務(wù)器資源,而且會出現(xiàn)Session丟失的不穩(wěn)定現(xiàn)象,所以一般的站點都選擇restful的無狀態(tài)服務(wù);
2、使用較為復(fù)雜的數(shù)據(jù)結(jié)構(gòu),比如字典里面嵌套字典,字典的鍵和值也使用字典,曾經(jīng)碰到過一個非常奇葩的項目,至少5層字典嵌套…有人會反駁說字典是引用類型,而且自動垃圾回收等等等等等等,在OOM面前一切雄辯都蒼白無力;
3、過深的繼承鏈,這里尤其要說的是類繼承,熟悉垃圾回收的應(yīng)該都清楚GC回收原理,繼承的存在有可能延長類的生命周期而不利于及時回收,所以,如果實際項目中出現(xiàn)繼承的深度超過兩位數(shù),那一定是抽象出現(xiàn)問題了,重構(gòu)是必然選擇;
4、一些多媒體處理程序的開發(fā)中內(nèi)存泄露情況也非常常見,比如使用GDI+開發(fā)畫圖程序等等,內(nèi)存消耗嚴(yán)重,這時候托管代碼開啟dispose模式無比重要;
5、在使用lucene.net的過程中發(fā)現(xiàn)有時候創(chuàng)建索引會出現(xiàn)OOM,數(shù)據(jù)量上去以后,內(nèi)存不足幾乎不可避免,這個時候就必須考慮重新調(diào)整架構(gòu)拆分索引文件分布處理了;
6、有時候調(diào)用office組件進行一些報表處理,發(fā)現(xiàn)內(nèi)存好像一下子少了好多?使用7z壓縮組件,如果多線程調(diào)用,好像也有內(nèi)存吃緊的現(xiàn)象?
7、調(diào)用第三方郵件組件處理郵件和附件,CPU和內(nèi)存開銷都很不能讓人滿意;
……
更多其他容易導(dǎo)致OOM的開發(fā)經(jīng)驗等你來補充。
六、警惕大對象
本文前面分析的幾種情況流于經(jīng)驗和表象,還有一種直達問題本質(zhì)的內(nèi)存泄露原因需要分析。
如果你深入理解了內(nèi)存回收原理以及大對象和大對象堆(Large Object Heap,LOH),那么大對象導(dǎo)致的內(nèi)存碎片化問題就很好理解了。
簡單來說:
1、任何大于等于85000字節(jié)的對象都被自動視為大對象,大對象從特殊的大對象堆中分配。大對象堆和小對象堆一樣進行終結(jié)和釋放,但是GC回收算法從來不對大對象堆(Large Object Heap)進行內(nèi)存壓縮整理,因為在堆中下移85000字節(jié)或更大的內(nèi)存塊會浪費太多的CPU時間;
2、在.NET中,CLR采用基于代(generation)的垃圾回收,大對象總被認(rèn)為是第2代(generation)的一部分,GC分析哪些對象不可達,優(yōu)先分析第0代和第1代,第2代的對象通常被認(rèn)為長時間存活。
正是由于1和2所述的兩個原因(主要原因還是第1個),在垃圾回收過程中容易造成內(nèi)存碎片。這里推薦一篇老外寫的流傳甚廣的文章供參考:the dangers of the LOH
隨著應(yīng)用程序的運行,如果LOH導(dǎo)致的內(nèi)存碎片越來越多,內(nèi)存有效使用率下降會非常嚴(yán)重,比如我們在web應(yīng)用程序中+=拼接字符串(見第4條的分析),如果大于等于85000字節(jié)的字符串臨時對象很多,那么用戶量一上去,隨著系統(tǒng)的運行,GC回收壓力越來越大OOM的風(fēng)險會變得更高。
雖然內(nèi)存碎片化導(dǎo)致的OOM看上去似乎無解,但是如果寫程序的人仔細分析解決問題,想方設(shè)法主動降低創(chuàng)建大對象的頻率,那么內(nèi)存泄露的可能就會降低,足夠優(yōu)秀健壯的程序不能徹底解決OOM,但是我們完全可以將風(fēng)險發(fā)生的情況將至最低可能。
一個足夠合格的coder肯定需要具備充足的分析和解決OOM問題的準(zhǔn)備和經(jīng)驗,有很多分析和檢查OOM的工具如ANTS Memory Profiler,還可以通過調(diào)試?yán)魅鐆indbg對內(nèi)存dump文件進行分析。用好這些工具,讓OOM無所遁形也不失為解決之道。