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