有效預防.NET應用程序OOM的經驗備忘


根據個人的開發和系統調優經驗,大部分的內存溢出(及內存泄漏)都和不好的開發習慣有直接關系,有幾個開發經驗可以有效預防OOM,總結下貼出來和大家分享。

 

一、批量和分頁

老生常談的話題,簡單,但是非常實用。

每個合格的coder對數據的處理,必須要有分頁或批量多次的意識。大數據量的讀取或查詢結果集是內存占用大戶,是系統性能下降的直接原因之一。

在典型的互聯網web應用中,數據量較大且高並發的情況下,不分頁,或者不進行批量處理,每次總是取出很多用戶數據,很容易造成內存開銷過大,系統內存吃緊。再比如我們有時候進行文件操作,讀取文件內容的時候就要斟酌考慮文件有多大。

如果你的項目中還在出現不分青紅皂白一次查詢返回N(N有多大?)條記錄的DataSet、DataTable或者列表記錄等等情況,或者查詢大量數據寫入臨時表,或者一次讀取很大文件內容......呵呵。

 

二、慎用靜態

這個也是常見但是比較隱式的引起內存泄露的元凶之一。

靜態類、靜態字段、靜態屬性,靜態委托,靜態方法…靜態的好處當然顯而易見,比如調用方便,常駐內存提高性能等等,所以,有些代碼索性靜態到底,除了實體層,在表現層(說起來非常可怕,我曾經在web應用程序中看到竟然有人名目張膽地大肆使用靜態字段),數據訪問層、業務邏輯層、公共組件、配置管理等等等等,能靜態的全給它用上靜態。

比起大數據查詢造成的常發性的內存不足,使用靜態太多的應用程序一時半會也不會內存泄露。可隨着系統的運行,靜態的東西越來越多,內存開銷也就越大,由於GC的回收策略,無效的靜態所占內存又不容易及時釋放,久而久之就造成了內存不足。

使用靜態的情況在分層應用程序中非常常見,而且由於它的好處容易得到體現而隱藏的風險不容易暴露出來,所以很多程序員還是非常執着地使用靜態。

 

三、二方庫、三方庫,非托管資源,優先使用Dispose模式

有些應用程序需要借助包裝的二方庫或者三方庫,或者使用了非托管資源,如com組件等等,由於.NET自動內存管理和回收,很多人覺得我用一下完成功能就Over了。

實際情況是你調用了別人的庫,別人的庫也很可能當仁不讓地占用了你的內存而你還不自知。

每次調用別人的資源都應該有個警覺性:用你的類庫可以,占用我的(內存)不行。

如果你熟悉自動內存管理,熟悉GC,理解Dispose模式,那么一定會在調用別人的資源的時候想着還是using一下為妙,或者,強制賦個null也是舉手之勞,要相信某些良好的編程習慣可以讓自動內存管理更有效。怕的就是很多人拿來主義,測試不充分,自己調用成功功能完成開發就OK了,交接給別人自己走人。

 

四、減少字符串臨時對象

這個實在是太熟悉不過了,不論是什么形式的應用程序,哪里能少得了字符串的身影?

看到它們有人條件反射似地想到拼接字符串,想到駐留池等等等等。

沒錯,不合理地使用String進行操作也會造成內存不足異常,而且這絕不是聳人聽聞。

舉例來說,對於String的+=,很多應用程序中這個操作層出不窮。我們都知道+=操作會造成很多臨時字符串對象,這些對象由於CLR對字符串的駐留處理,容易占用內存空間。如果是高並發的web應用程序,而字符串操作隨處可見,且字符串的長度又不確定地長,前端頁面各種各樣的拼接,久而久之,內存占用就會是一個重大問題。CLR對字符串的優化處理使得字符串不被優先回收,如果字符串操作頻繁,臨時字符串較長(比如大於等於85000字節)而出現大對象堆的分配,那么更容易出現內存泄露。

很多人可能都會想到如何優化程序去降低string的臨時對象的生成概率。對的,StringBuilder的出現就順理成章了。

 

五、其他經驗

1、Session的不當使用,尤其是使用InProc模式的會話,為了保持狀態而選擇使用Session,如用戶訪問量較大將極大消耗服務器資源,而且會出現Session丟失的不穩定現象,所以一般的站點都選擇restful的無狀態服務;

2、使用較為復雜的數據結構,比如字典里面嵌套字典,字典的鍵和值也使用字典,曾經碰到過一個非常奇葩的項目,至少5層字典嵌套…有人會反駁說字典是引用類型,而且自動垃圾回收等等等等等等,在OOM面前一切雄辯都蒼白無力;

3、過深的繼承鏈,這里尤其要說的是類繼承,熟悉垃圾回收的應該都清楚GC回收原理,繼承的存在有可能延長類的生命周期而不利於及時回收,所以,如果實際項目中出現繼承的深度超過兩位數,那一定是抽象出現問題了,重構是必然選擇;

4、一些多媒體處理程序的開發中內存泄露情況也非常常見,比如使用GDI+開發畫圖程序等等,內存消耗嚴重,這時候托管代碼開啟dispose模式無比重要;

5、在使用lucene.net的過程中發現有時候創建索引會出現OOM,數據量上去以后,內存不足幾乎不可避免,這個時候就必須考慮重新調整架構拆分索引文件分布處理了;

6、有時候調用office組件進行一些報表處理,發現內存好像一下子少了好多?使用7z壓縮組件,如果多線程調用,好像也有內存吃緊的現象?

7、調用第三方郵件組件處理郵件和附件,CPU和內存開銷都很不能讓人滿意;

8、不斷地注冊事件(Event)  請參考Artech的這篇這篇

……

更多其他容易導致OOM的開發經驗等你來補充。

 

六、警惕大對象

本文前面分析的幾種情況流於經驗和表象,還有一種直達問題本質的內存泄露原因需要分析。

如果你深入理解了內存回收原理以及大對象和大對象堆(Large Object Heap,LOH),那么大對象導致的內存碎片化問題就很好理解了。

簡單來說:

1、任何大於等於85000字節的對象都被自動視為大對象,大對象從特殊的大對象堆中分配。大對象堆和小對象堆一樣進行終結和釋放,但是GC回收算法從來不對大對象堆(Large Object Heap)進行內存壓縮整理,因為在堆中下移85000字節或更大的內存塊會浪費太多的CPU時間;

2、在.NET中,CLR采用基於代(generation)的垃圾回收,大對象總被認為是第2代(generation)的一部分,GC分析哪些對象不可達,優先分析第0代和第1代,第2代的對象通常被認為長時間存活。

正是由於1和2所述的兩個原因(主要原因還是第1個),在垃圾回收過程中容易造成內存碎片。這里推薦一篇老外寫的流傳甚廣的文章供參考:the dangers of the LOH

隨着應用程序的運行,如果LOH導致的內存碎片越來越多,內存有效使用率下降會非常嚴重,比如我們在web應用程序中+=拼接字符串(見第4條的分析),如果大於等於85000字節的字符串臨時對象很多,那么用戶量一上去,隨着系統的運行,GC回收壓力越來越大OOM的風險會變得更高。

雖然內存碎片化導致的OOM看上去似乎無解,但是如果寫程序的人仔細分析解決問題,想方設法主動降低創建大對象的頻率,那么內存泄露的可能就會降低,足夠優秀健壯的程序不能徹底解決OOM,但是我們完全可以將風險發生的情況將至最低可能。

一個足夠合格的coder肯定需要具備充足的分析和解決OOM問題的准備和經驗,有很多分析和檢查OOM的工具如ANTS Memory Profiler,還可以通過調試利器如windbg對內存dump文件進行分析。用好這些工具,讓OOM無所遁形也不失為解決之道。

最后,本文是今年春節前的最后一篇,提前祝大家節日快樂。文章寫得太精彩了,完全可以用流水賬來形容╮(╯_╰)╭,自我鼓勵一下結束,哈哈。

 

參考:

<<CLR via C#>>

the dangers of the LOH

<<深入理解Java虛擬機>>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM