在探討這個技術(shù)之前寂恬,我們先看一段代碼:
1 . 這段代碼就是窗體加載時利用File靜態(tài)類實(shí)現(xiàn)創(chuàng)建一個txt文件
2 . 隨后點(diǎn)擊窗體上的button創(chuàng)建一個針對以上創(chuàng)建txt文件的文件流對象
我們來運(yùn)行看看初肉,首先彈出Demo窗體,這是可以看到桌面上新建了一個text.txt的文本文件臼隔,隨后我們點(diǎn)擊窗體上的button1的按鈕之后
這時有經(jīng)驗(yàn)的小伙伴可能要說了摔握,F(xiàn)ile打開了沒有釋放資源,要先釋放資源泊愧。
如果我在原先代碼中增加如下代碼之后呢盛正?
運(yùn)行之后發(fā)現(xiàn)蛮艰,Demo可以正常運(yùn)行,并沒有報任何異常了即寡。
接下來袜刷,我們不增加任何代碼著蟹,只是更改下Test()在代碼中的位置
運(yùn)行之后,報了同樣的異常:
我的天啊涮雷!這是咋回事洪鸭?
接下來就引出今天的技術(shù)主題,希望通過本章的探究之后置鼻,小伙伴們可以明白這是怎么回事兒……
一. 何為垃圾回收 (GC-Garbage Collector)
物理內(nèi)存:存放應(yīng)用程序運(yùn)行期間所產(chǎn)生的也是必須的信息資源蜓竹,也即是二進(jìn)制信息的集合储藐。內(nèi)存是寶貴的資源邑茄,好東西當(dāng)然要用到刀刃上俊啼,經(jīng)不起浪費(fèi)授帕,如果不處理好垃圾回收浮梢,就會時常遇到OutOfMemory的報錯-內(nèi)存溢出,這對操作系統(tǒng)以及應(yīng)用程序的使用是極大的傷害芥映,所以及時回收垃圾內(nèi)存是必不可少的關(guān)鍵機(jī)制远豺。
二. 垃圾回收機(jī)制
內(nèi)存資源分配
托管資源(棧躯护,托管堆CLR-GC自動回收,是由操作系統(tǒng)決定回收時機(jī))
非托管資源(非托管堆 Follow C/C++ 的手動釋放)
托管資源回收機(jī)制
.Net中80%都是托管資源裁蚁,比如為我們所熟知的值類型與引用類型继准,而值類型是直接分配在內(nèi)存的棧區(qū)域移必,這塊區(qū)域的內(nèi)存是用完即彈出的,所以不需要任何額外的工作去參與回收舞萄,而引用類型是分配在內(nèi)存的托管堆區(qū)域管削,這塊區(qū)域是由CLR負(fù)責(zé)分配與回收的含思。這里就要讓大名鼎鼎的GC(垃圾回收器)出場了甘晤,首先GC是系統(tǒng)級的一個線程饲做,即它是由系統(tǒng)來調(diào)用盆均,那么系統(tǒng)何時回去調(diào)用這個GC呢?一定是有某種機(jī)制來保證這個功能的運(yùn)作游沿,對的肮砾,那就是GC首先會去掃描托管堆內(nèi)存中的所有對象引用仗处,只要某對象不可達(dá)(即沒有任何root引用到該對象),那么這個對象就會被標(biāo)記(標(biāo)記為垃圾回收的目標(biāo))吃环,在這個GC機(jī)制中有一個很重要的概念那就是GC的代(就是等級的意思)洋幻,這個代的機(jī)制的引入主要是為了提高性能鞋屈,以避免每次回收整個托管堆造成的性能損失,這里具體就不介紹了渠啊。最后總結(jié)一下GC的特性:
1) GC只會自動管理托管內(nèi)存資源的回收权旷,它是不能夠自動管理釋放非托管資源的拄氯;
2) GC并不是實(shí)時性的,這將會造成系統(tǒng)性能上的拼瓶頸與不確定性镣煮。
非托管資源回收機(jī)制
其他資源比如窗口句柄鄙麦,數(shù)據(jù)庫連接,字節(jié)流恨胚,文件流炎咖,GDI+相關(guān)對象乘盼,COM對象,Pen等等是屬于非托管資源,這里需要注意的是猴娩,為啥數(shù)據(jù)庫連接我沒說SqlConnection卷中,文件流我沒說FileStream,字節(jié)流我沒說BinaryStream等等议忽,其實(shí)嚴(yán)格意義上來說十减,SqlConnection帮辟,F(xiàn)ileStream,BinaryStream之類的并不能稱之為非托管資源芍锚,其實(shí)他們是托管類蔓榄,但是這些托管類當(dāng)中卻使用了非托管資源甥郑,所以就資源來說,就是數(shù)據(jù)庫連接嗅钻,文件流养篓,字節(jié)流。而SqlConnection,FileStream,BinaryStream就是使用了非托管資源的托管類舶胀。在我們實(shí)際開發(fā)過程中碧注,更多遇到的就是這些使用非托管資源的托管類(所以后續(xù)所談的關(guān)于非托管資源回收也就是針對這種情況)萍丐。單純純粹的使用那些非托管資源是很少的,這些非托管資源是分配在非托管內(nèi)存中基茵,而不是前面所說的托管內(nèi)存中拱层,所以非托管資源的回收GC是無法插手的宴咧,那這就得有程序自己去做好回收處理了掺栅。那么牛逼的.Net Framework有沒有提供給我們釋放非托管資源的方式呢,很顯然茬高,MS是不會讓我們失望的假抄,她提供了2種方式宿饱,一種就是類型自帶Finalize()方法,另一種就是實(shí)現(xiàn)IDisposable接口的Dispose方法强饮,相較于GC來說邮丰,非托管資源的回收權(quán)掌握在我們自己手里,那么我們可就要好好搗鼓搗鼓娃循,要不然沒強(qiáng)大的GC給我們擦屁股斗蒋,我們自己是很容易犯錯的泉沾,動不動你可能就會遇到你的應(yīng)用程序內(nèi)存暴漲跷究,性能低下甚至程序無故崩潰的惡心后果。所以接下來我們就來分析分析這兩種方式的使用:
1) Finalize方法
在.Net的基類System.Object中丁存,定義了名為Finalize()的一個虛方法潭袱,這個方法默認(rèn)啥都不做屯换。
顧名思義Finalize: 終結(jié)的意思彤悔,即指工作收尾晕窑,清場的意思卵佛。所以很顯然這個函數(shù)就是提供我們清理資源的一個入口截汪,那么這個方法是誰去調(diào)用的呢?是系統(tǒng)有個機(jī)制去調(diào)用亦或是我們程序自己去調(diào)用呢阳柔?很開心的告訴你舌剂,是系統(tǒng)去調(diào)用,小伙伴們聽到后很開心有木有荐绝,終于又可以省下一筆時間好好的喝喝茶看看報了順帶玩把跳一跳了 _谴忧。(凡事都有兩面性哦沾谓,正因?yàn)槭遣僮飨到y(tǒng)做均驶,那就不敢保證實(shí)時性與確定性嘍,.Net大大很給力的爬虱,后面又提供了另外一種方式腾它,對啦瞒滴,就是后面我們將要講的IDisposable)
言歸正傳妓忍,那系統(tǒng)又是如何調(diào)用Finalize方法的呢,所以下面我們來談?wù)凢inalize的工作機(jī)制:
i) CLR在托管堆上分配對象空間的時候定罢,會自動確定該對象是否提供一個自定義的Finalize方法祖凫,如果檢測到有的話酬凳,那么這個對象就會被標(biāo)記為可終結(jié)的粱年,同時一個指向這個對象的指針就會被保存到一個名字為終結(jié)隊(duì)列的內(nèi)部隊(duì)列中,終結(jié)隊(duì)列是有GC維護(hù)的一張表(小伙伴們是不是很親切啊完箩,對的弊知,看到GC啦),這種表指向每一個在從堆上刪除之前必須終結(jié)的對象叔扼。
ii) 當(dāng)GC確定要從內(nèi)存中釋放某個對象的時候瓜富,它會檢查終結(jié)隊(duì)列上的每一項(xiàng)降盹,并將對象放到一個隊(duì)列中(從終結(jié)隊(duì)列移到foreachable隊(duì)列)中去蓄坏,然后啟動另外一個獨(dú)立線程(我們稱之為Finalizer線程)而不是GC線程來執(zhí)行這些Finalizer(下個GC周期時),GC線程會繼續(xù)刪除其他待回收的對象结蟋,而是在下一個GC周期嵌屎,F(xiàn)inalizer線程才去回收這些對象胳岂,由此可見乳丰,實(shí)現(xiàn)了Finalize方法的對象必須等待兩次GC才能被完全釋放产园,所以這些對象某種意義上是會在GC中自動“延長”生存周期夜郁。從上面可以看出竞端,F(xiàn)inalize方法的調(diào)用是蠻耗費(fèi)資源的,F(xiàn)inalize方法的作用是保證.Net對象能夠在垃圾回收時清理非托管資源技俐,如果創(chuàng)建了一個不使用非托管資源的類型雕擂,實(shí)現(xiàn)終結(jié)器是沒有任何意義的井赌,所以沒有特殊的需求應(yīng)該要避免重寫Finalize方法。
看到這流部,是不是有一些好學(xué)的小伙伴屁顛屁顛的跑去VS上給某個使用了非托管資源的類型重新Finalize方法贵涵,一編譯宾茂,臥槽拴还,編譯失敗
其實(shí)片林,當(dāng)我們想重寫Finalize方法時费封,C#為我們?yōu)槲覀兲峁┝宋鰳?gòu)函數(shù)這種語法來重寫該方法弓摘,為毛要這樣曲折呢,感興趣的朋友可以研究研究(也可以在文章結(jié)尾處多注意注意哈_)末患,析構(gòu)函數(shù)語法跟構(gòu)造函數(shù)類似璧针,但析構(gòu)函數(shù)有個前綴~渊啰,并且不能加任何訪問修飾符,不能加任何參數(shù)独柑,不能重載忌栅,所以一個類只能有一個析構(gòu)函數(shù)曲稼,也叫終結(jié)器贫悄。
2) IDisposable接口
記性好的小伙伴們應(yīng)該還記得上文有提到過這個茬窄坦,那就是通過垃圾回收是可以利用對象的終結(jié)器來釋放非托管資源。然后彤侍,很多非托管資源非常寶貴盏阶,比如數(shù)據(jù)庫連接以及文件句柄闻书,所以他們應(yīng)該盡可能快的被回收資源魄眉,而不能依靠垃圾回收來被動處理坑律,為了更及時的對這些非托管資源進(jìn)行回收脾歇,進(jìn)而.Net提供了另外一種方式—IDisposable接口淘捡,跟垃圾回收的被動處理不同焦除,此接口是提供給了我們主動回收的方式,這樣就能如我們所愿主動及時的去回收那些非托管資源了,哈哈乌逐,我又要說那句富有哲理的老話啦竭讳,凡事都有兩面性的,小伙伴們是不是都有想打我的沖動啦浙踢,Are you kidding us???小伙伴們稍安勿躁绢慢,人生在世,切忌浮躁哦洛波,人生就是這樣胰舆,凡事都有好有壞,世事無常蹬挤,找到一個合適的平衡點(diǎn)對于人生是很重要的……扯遠(yuǎn)啦,回到正題來焰扳,為什么說這種方式也具有兩面性呢倦零,因?yàn)檫@種方式是我們自己顯式去調(diào)用,是人那就會犯錯吨悍,所以丟掉忘記那是很有可能的事扫茅,可能會漏掉Dispose的調(diào)用也有可能是在調(diào)用Dispose之前出現(xiàn)了異常,那么有些資源可能就一直留在內(nèi)存中了育瓜,除非你通過工具手動清除或重啟電腦诞帐,為最大程度的避免這種疏忽,我們可以使用try catch finally這種方式保證Dispose確實(shí)會被調(diào)用到爆雹,但每次套個try catch finally會覺得很麻煩停蕉,故此C#為我們提供了using關(guān)鍵字來簡化Dispose的調(diào)用,其實(shí)實(shí)質(zhì)上就是try catch finally的模式钙态,只不過C#做了語法糖慧起,讓我們寫起來更簡潔,所以任何實(shí)現(xiàn)了IDisposable接口的類型册倒,都可以用using語句蚓挤,沒有的話,那直接就會編譯報錯啦驻子。
從前面的介紹了解到灿意,F(xiàn)inalize可以通過垃圾回收進(jìn)行自動的調(diào)用,而Dispose需要被代碼顯示的調(diào)用崇呵,所以缤剧,為了保險起見,對于一些非托管資源域慷,還是有必要實(shí)現(xiàn)終結(jié)器的荒辕。也就是說汗销,如果我們忘記了顯示的調(diào)用Dispose,那么垃圾回收也會調(diào)用Finalize抵窒,從而保證非托管資源的回收弛针。
其實(shí),MSDN上給我們提供了一種很好的模式來實(shí)現(xiàn)IDisposable接口來結(jié)合Dispose和Finalize李皇,例如下面的代碼:
class MyResourceWrapper : IDisposable
{
private bool IsDisposed = false;
public void Dispose()
{
Dispose(true);
//tell GC not invoke Finalize method
GC.SuppressFinalize(this);
}
protected void Dispose(bool Disposing)
{
if (!IsDisposed)
{
if (Disposing)
{
//clear managed resources
}
//clear unmanaged resources
}
IsDisposed = true;
}
~MyResourceWrapper()
{
Dispose(false);
}
}
在這個模式中削茁,void Dispose(bool Disposing)函數(shù)通過一個Disposing參數(shù)來區(qū)別當(dāng)前是否是被Dispose()調(diào)用。如果是被Dispose()調(diào)用掉房,那么需要同時釋放托管和非托管的資源付材。如果是被終結(jié)器調(diào)用了,那么只需要釋放非托管的資源即可圃阳。Dispose()函數(shù)是被其它代碼顯式調(diào)用并要求釋放資源的厌衔,而Finalize是被GC調(diào)用的。
另外捍岳,由于在Dispose()中已經(jīng)釋放了托管和非托管的資源富寿,因此在對象被GC回收時再次調(diào)用Finalize是沒有必要的,所以在Dispose()中調(diào)用GC.SuppressFinalize(this)避免重復(fù)調(diào)用Finalize锣夹。同樣页徐,因?yàn)镮sDisposed變量的存在,資源只會被釋放一次银萍,多余的調(diào)用會被忽略变勇。
所以這個模式的優(yōu)點(diǎn)可以總結(jié)為:
如果沒有顯示的調(diào)用Dispose(),未釋放托管和非托管資源贴唇,那么在垃圾回收時搀绣,還會執(zhí)行Finalize(),釋放非托管資源戳气,同時GC會釋放托管資源
如果調(diào)用了Dispose()链患,就能及時釋放了托管和非托管資源,那么該對象被垃圾回收時瓶您,就不會執(zhí)行Finalize()麻捻,提高了非托管資源的使用效率并提升了系統(tǒng)性能
通過以上的探究,現(xiàn)在回到文章一開始遇到的那個問題呀袱,我們就可以知道實(shí)際上File.Create方法返回的是一個FileStream實(shí)例:
然而這個實(shí)例其實(shí)就是一個使用了非托管資源的托管類贸毕,而文章一開始的例子當(dāng)中,在創(chuàng)建完File之后并沒有及時的去回收掉這個FileStream實(shí)例夜赵,所以他只能等待GC的自動回收明棍,然后GC的回收機(jī)制是不實(shí)時和不確定的,所以當(dāng)我們緊接著去針對這個文件創(chuàng)建一個文件流的時候GC此時還并沒有去回收她油吭,所以就會出現(xiàn)占用的異常击蹲,而增加Test()這個方法主要是為了故意增加內(nèi)存的使用,逼迫系統(tǒng)進(jìn)行一次垃圾回收(當(dāng)然我們也可以通過GC.Collect()來做)婉宰,所以之后就不會再出現(xiàn)這個異常歌豺,而為什么將Test()代碼位置變動一下之后也會出現(xiàn)異常呢,這個就是上面提到的GC代的概念心包,有興趣的朋友可以自行了解下类咧。正如上面總結(jié)的那樣,對于使用了非托管資源的類型蟹腾,我們需要及時手動的進(jìn)行回收動作痕惋。
末尾彩蛋:之所以C#只支持析構(gòu)方式進(jìn)行Finalize方法的重寫,是因?yàn)镃#編譯器會為Finalize方法隱式地加入一些必需的基礎(chǔ)代碼娃殖。下面就是我們通過ILSpy查看到了IL代碼值戳,F(xiàn)inalize方法作用域內(nèi)的代碼被放在了一個try塊中,然后不管在try塊中是否遇到異常炉爆,finally塊保證了Finalize方法總是能夠被執(zhí)行堕虹。
/**以上僅為個人學(xué)習(xí)總結(jié),轉(zhuǎn)載請標(biāo)注原處芬首,如有不足之處請指正
搞技術(shù)赴捞,我們是認(rèn)真的。*****/