分享.net常見的內存泄露及解決方法
關于內存泄漏的問題,之前也為大家介紹過,比如:檢測C++中的內存泄漏,是關于C++內存泄漏的。今天為大家介紹的是關于.NET內存泄漏的問題。
前段時間幫項目組內做了一次內存優化,產品是使用c#開發的winForm程序,一直以為.net提供了垃圾收集機制,開發的時候也沒怎么注意內存的釋放,導致最后的產品做出來之后,運行一個多小時就內存直接崩潰了,看來.net的垃圾收集還是得需要開發者加以控制,也不是萬能的啊。
下面將對垃圾收集做以簡介,然后描述一下我在內存優化過程中常見的內存泄露及解決方法。
托管堆的內存分配(下文中的托管堆指的是GC堆)
托管堆是以應用程序域為依托的,即每一個應用程序域有一個托管堆,每一個托管堆也只屬于一個應用程序域,且托管堆是一塊連續的內存,其中的對象也是緊密排列的。相對于C++中的非連續內存堆來說,托管堆的內存分配效率要高。托管堆維護了一個指針,指向當前已使用內存的末尾,當需要分配內存的時候,只需要指針向后移動指定數量的位置即可。而且托管堆通過應用程序域實現了應用程序之間內存的隔離,即不同的應用程序域之間在正常情況下是不能相互訪問各自的托管堆的。
垃圾收集
垃圾收集的算法有很多。例如引用計數、標記清除等等,托管堆使用的標記清除算法。
托管堆使用的是分代標記清除算法。
標記清除算法
首先,系統將托管堆內所有的對象視為可以回收的垃圾,然后系統從GCRoot開始遍歷托管堆內所有的對象,將遍歷到的對象標記為可達對象,在遍歷完成之后,回收所有的非可達對象,完成一遍垃圾收集。
注意,托管堆的垃圾收集只會自己收集托管對象!
由于在執行完垃圾收集之后,托管堆中會產生很多的內存碎片,導致內存不再連續,因此在垃圾收集完成之后,系統會執行一次內存壓縮,將不連續的內存重新排列整齊,變成連續的內存。(關于垃圾收集的詳細信息,大家可以參考《CLR Via C#》)
通過上面的簡述,大家都知道什么樣的對象不會被收集,即能從GCRoot開始遍歷到的對象。
最常見的GCRoot是線程的棧,線程的棧里面通常包含方法的參數、臨時變量等。另外常見的GCRoot還有靜態字段、CPU寄存器以及LOH堆上的大的集合。因此,如果想要讓托管對象的內存順利的釋放,只需要斷開與跟之間的聯系即可。而對于非托管對象的內存,必須進行手動釋放。
下面我根據自己在優化內存的過場中的一些常見錯誤以及一些解決方法。
事件
在.net內存泄露的原因當中,事件占據了非常大的一部分比例,事件是一種委托的實例,也就是與我們類中其他的字段一樣,也是一個字段。
委托為什么能阻止垃圾收集呢?即委托是如何讓相關的對象在垃圾收集的時候被標記為可達對象的呢?首先要從委托的本質看起,
我們通常使用的委托是從類
- public abstract class MulticastDelegate : Delegate
繼承的,MulticastDelegate內部維護了一個private object _invocationList;,即我們通常所有的委托鏈(ps:委托鏈同字符串一樣,是不可變的),這個委托鏈是以個object [],內部保存了Delegate對象,及每一個委托實際上是一個Delegate對象,而Delegate包含了兩個非常重要的字段:
- internal object _target;
- internal object _methodBase;
其中_target就是訂閱事件的對象,_methodBase則是訂閱事件的方法的 MethodInfo。其關聯關系如下例所示:
- Code:
- public event EventHandler TestEvent;
- void MethodEndTempVarClear()
- {
- Test tempTestEvent = new Test();
- TestEvent += tempTestEvent.TestEvent;
- }
我們假設此段代碼所在的對象即為一個可達的對象,則其引用關系如下圖所示:
由上圖我們可以看出,原本應該在方法結束后就可以變為不可達對象的tempTestEvent變成了可達對象,因此也不能對其進行收集了。
個人建議:將類中所有的事件訂閱添加到一個專門的方法當中,且實現一個與其匹配的取消訂閱的方法,并在必要的時候,調用取消訂閱的方法。
非托管對象
非托管對象無論在什么時候,都不會被垃圾收集所回收,必須手動釋放。
.net中的非托管資源都實現了IDispose接口,我們可以在使用的時候,使用using(){}類實現非托管資源的釋放。
其中有一種情況非常容易遺漏,即通過一個方法創建了一個非托管的對象,如下所示:
- public MemoryStream CreateAStream()
- {
- return new MemoryStream();
- }
大家在使用的時候非常容易遺忘通過這種方法的形式創建的非托管對象,尤其是一些名字意義表達不準確的時候,例如
- var temp = CreateATemp();//CreateATemp返回一個非托管對象
大家可能會漏掉對temp的內存釋放,因此建議大家盡量少用方法創建或者初始化非托管對象,如果需要,則使用如下的方式:
- bool InitializeStream(out MemoryStream stream)
- {
- stream = new MemoryStream();
- return true;
- }
即使用out關鍵字,這樣大家在使用這個方法的時候,需要首先聲明相關的非托管對象,可以在使用完成之后,及時的釋放,減少遺漏。
集合/靜態字段
對于集合,我們在使用完成之后,需要即時的clear,尤其是將一些方法中的臨時變量添加到集合當中之后,會導致集合膨脹,并使得其中的內存泄露。
對于靜態字段,我們應該盡量減少其可見的域,因為靜態字段在整個程序運行期間都不會被釋放,減少其可見域就減少了其內存泄露的可能性,注意,不到萬不得以,千萬不要聲明靜態的集合,就是使用了,那也一定要小心再小心。靜態集合很容易造成內存泄露。
最好,大家有什么好的建議后者方法,歡迎補充!!
【編輯推薦】