技術(shù)探究 – 垃圾回收機(jī)制 via C#

在探討這個技術(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)存資源分配

  1. 托管資源(棧躯护,托管堆CLR-GC自動回收,是由操作系統(tǒng)決定回收時機(jī))

  2. 非托管資源(非托管堆 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)真的。*****/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末郁稍,一起剝皮案震驚了整個濱河市赦政,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌耀怜,老刑警劉巖恢着,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異财破,居然都是意外死亡然评,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進(jìn)店門狈究,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碗淌,“玉大人,你說我怎么就攤上這事抖锥∫诿撸” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵磅废,是天一觀的道長纳像。 經(jīng)常有香客問我,道長拯勉,這世上最難降的妖魔是什么竟趾? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任憔购,我火速辦了婚禮,結(jié)果婚禮上岔帽,老公的妹妹穿的比我還像新娘玫鸟。我一直安慰自己,他們只是感情好犀勒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布屎飘。 她就那樣靜靜地躺著,像睡著了一般贾费。 火紅的嫁衣襯著肌膚如雪钦购。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天褂萧,我揣著相機(jī)與錄音押桃,去河邊找鬼。 笑死导犹,一個胖子當(dāng)著我的面吹牛怨规,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播锡足,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼波丰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了舶得?” 一聲冷哼從身側(cè)響起掰烟,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎沐批,沒想到半個月后纫骑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡九孩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年先馆,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片躺彬。...
    茶點(diǎn)故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡煤墙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宪拥,到底是詐尸還是另有隱情仿野,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布她君,位于F島的核電站脚作,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜球涛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一劣针、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧亿扁,春花似錦捺典、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肝箱。三九已至哄褒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間煌张,已是汗流浹背呐赡。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留骏融,地道東北人链嘀。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像档玻,于是被迫代替她去往敵國和親怀泊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 1.什么是垃圾回收误趴? 垃圾回收(Garbage Collection)是Java虛擬機(jī)(JVM)垃圾回收器提供...
    簡欲明心閱讀 89,493評論 17 311
  • 1. 垃圾回收的意義在C++中霹琼,對象所占的內(nèi)存在程序結(jié)束運(yùn)行之前一直被占用,在明確釋放之前不能分配給其它對象凉当;而在...
    愛情小傻蛋閱讀 933評論 0 11
  • 來自: Android夢想特工隊(duì)作者: Aaron主頁: http://www.wxtlife.com/原...
    技術(shù)特工隊(duì)閱讀 4,373評論 0 28
  • 20170528 【情緒】對生活和工作失去了掌控感枣申,混亂且缺乏秩序,焦慮看杭,想逃避忠藤,又渴望回到正軌,但又力不從心楼雹,常...
    想飛的小粉豬閱讀 209評論 0 0
  • 一花一世界模孩,一葉一菩提。 我愛花贮缅,也把養(yǎng)花當(dāng)作生活中的一種樂趣瓜贾。雖然許多花的品種我也叫不上來名字,可是携悯,看著那一簇...
    夏天free閱讀 1,412評論 0 2