程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 【C#進階系列】20 托管堆和垃圾回收,

【C#進階系列】20 托管堆和垃圾回收,

編輯:C#入門知識

【C#進階系列】20 托管堆和垃圾回收,


托管堆基礎

一般創建一個對象就是通過調用IL指令newobj分配內存,然後初始化內存,也就是實例構造器時做這個事。

然後在使用完對象後,摧毀資源的狀態以進行清理,然後由垃圾回收器來釋放內存。

托管堆除了能避免錯誤使用已經被釋放的內存,也會減少內存洩漏,大多數類型都無需資源清理,垃圾回收器會自動釋放資源。

當然也有需要立即清理的,比如一些包含了本機資源的類型(如文件、套接字和數據庫連接等),可在這些類中調用一個Dispose方法。(當然有的類對這個方法封裝了一下,可能是別的名字比如斷開數據庫連接的close)

在托管堆上分配資源

CLR要求所有對象都從托管堆分配。

進程初始化時,CLR劃出一個地址空間區域作為托管堆。CLR還維護一個指針,即NextObjPtr。該指針指向下一個對象在堆中的分配未知。

一個區域被非垃圾對象填滿後,CLR會分配更多的區域,這個過程不斷重復,直到整個進程地址空間都被填滿。32位進程最多分配1.5G,64位進程最多分配8TB。

當創建一個對象時,首先會計算該對象類型字段(包括基類)所需字節數,加上對象的開銷所需的字節數(即類型對象指針和同步塊索引)。

然後CLR會檢查區域中是否有分配對象所需字節數大小的內存。如果托管堆有,那麼就在NextObjPtr指向的地址放入對象,且NextObjPtr會加上對象占用的字節數得到新值,即下個對象放入時的地址。

通過垃圾回收器(GC)回收資源

CLR在創建對象時發現沒有足夠內存分配該對象,那麼就會執行垃圾回收。

CLR在進行垃圾回收時,首先會暫停所有線程,

標記階段:然後CLR會遍歷堆中所有的引用對象,將同步塊索引字段中的一位設為0,表示所有對象都要刪除。然後檢查所有的活動根(即所有引用類型的字段以及方法的參數和局部變量),查看它們引用了哪些對象。任何根引用了堆上的對象,那麼CLR就標記那個對象,將同步塊索引字段中的位設為1,如果對象已經被標記為1了,那麼就不再重新檢查對象的字段。標記為1的也就是被引用的對象,稱為可達的,標記為0的就是不可達的。此時CLR就知道了哪些對象可以刪除,哪些對象不能刪除。

壓縮階段:CLR對堆中已標記的對象進行搬移內存位置(且對象所有的根的引用也自然會跟著變動),使得被標記的對象緊密相連,即占用連續的內存空間。這樣不僅減小了應用程序的工作集,從而提升了訪問性能,還得到了大量的未占用內存空間,並且解決了內存碎片化的問題。

最後,恢復所有線程。

靜態字段引用的對象一直存在,直到用於加載類型的AppDomain卸載為止。內存洩漏的一個常見原因就是讓靜態字段引用某個集合對象,然後不停地往集合添加數據項。靜態字段使集合對象一直存活,而集合對象使所有數據項一直存活。因此應該盡量避免使用靜態字段。(或者參照前面的玩法,當我們不用靜態變量的時候,可以立馬置為null,那麼垃圾就會被回收)。

有一個神奇的垃圾回收特例——Timer。原因是它會每隔一段時間去調用回調函數,但是根據之前學的垃圾回收玩法可以知道當Timer的變量離開了作用域,且沒有其它函數引用了Timer對象,那麼在垃圾回收時Timer就會被回收掉。也就不會去執行回調函數了。(所以說慎用Timer,這裡有這麼一個大坑)

代:提升性能

CLR的GC是基於代的垃圾回收器。它對代碼做了如下假設:

  • 對象越新,生存期越短
  • 對象越老,生存期越長
  • 回收堆的一部分,速度快於回收整個堆

第0代:新添加到堆的對象稱為第0代對象,垃圾回收器從未檢查過它們。

第1代:第0代對象經過一次垃圾回收,但是並沒有被當做垃圾釋放掉,那麼就會在壓縮階段一起放入第1代對象區域。

第2代:第1代對象又經過了一次垃圾回收,但是並沒有被當做垃圾釋放掉,那麼就會在壓縮階段一起放入第2代對象區域。沒有第3代,第2代放著的就是經過了2次和2次以上垃圾回收的對象。

第0代內存區域滿了就會進行垃圾回收,此時不僅會回收第0代的區域,還會去判斷第1代區域是否也滿了,滿了也回收第1代,不滿的話即時第1代裡面有不可達的對象,那麼也不會回收第1代。

CLR初始化時,會為這三代分別選擇內存預算,以此判斷什麼時候該回收了。但是CLR的垃圾回收器是自調節的。

也就是說

如果垃圾回收器發現第0代回收後存活下來的對象很少,那麼就會減少第0代的預算,這樣的話垃圾回收就會發生得更頻繁了,然而垃圾回收器每次做的事更少了,這減小了工作集。如果沒有一個存活的話,連壓縮都免了。

如果垃圾回收器發現第0代回收後存活下來的對象很多,那麼就會加大第0代的預算,這樣的話垃圾回收就會發生得不頻繁了,然而垃圾回收器每次回收的內存要多得多。(如果沒有回收到足夠的內存,那麼垃圾回收器會執行一次完整回收,如果還是沒有足夠內存,那麼就會拋出OutOfMemoryException異常)。

上面是用第0代舉例,第1、2代也如是。 

垃圾回收觸發條件

CLR在檢測第0代超過預算時會觸發一次GC,這是GC最常見的觸發條件,還有其它的觸發如下:

  • 代碼顯示調用System.GC的靜態Collect方法
  • Windows報告低內存情況
  • CLR正在卸載AppDomain
  • CLR正在關閉

大對象

CLR將對象分為大對象和小對象,以85000字節為界限。

大對象不是在小對象的地址空間分配,而是在進程地址空間的其它地方分配。

目前版本的GC不壓縮大對象,因為在內存中移動它們代價過高。(可能會造成空間碎片)

大對象總是第2代,所以只能為需要長時間存活的資源生成大對象,否則若短時間存活的大對象放在第二代中,因為之前講到一次回收過多內存,就會將代的預算減少,導致更頻繁回收第2代,會損害性能。

垃圾回收模式

CLR啟動時會選擇一個GC模式,進程終止前該模式不會改變:

  • 工作站模式
    • 該模式針對客戶端應用程序優化GC。GC造成延時很低,應用程序線程掛起時間很短,避免使用戶感到焦慮。在該模式下,GC假定機器上運行的其它應用程序都不會消耗太多的CPU資源。 
  • 服務器模式
    • 該模式針對服務器端應用程序優化GC。被優化的主要是吞吐量和資源利用。GC假定機器上沒有運行其它應用程序,並假定機器上所有的CPU都可以用來輔助完成GC。該模式造成托管堆被分為幾個區域(section),每個CPU一個。開始垃圾回收時,垃圾回收器在每個CPU上都運行一個特殊線程,每個線程和其他線程並發回收它自己的區域。對於工作者線程(worker thread)行為一致的服務器應用程序,並發回收能很好地進行。這個功能要求應用程序在多CPU計算機上運行,使線程能真正地同時工作嗎,從而獲得性能上的提升。

應用程序默認以工作站GC模式運行。寄宿了CLR的服務器應用程序(比如ASP.NET和Sql Server)可請求CLR加載“服務器”GC,但如果是單處理器計算機上運行,CLR將總是使用工作站GC模式。

獨立應用程序可在配置文件中,加上下面配置項告訴CLR使用服務器模式:

<configuration>
  <runtime>
    <gcServer enabled="true"/>
  </runtime>
</configuration>

除了這兩種模式,GC還支持兩種子模式:並發(默認)和非並發。

在並發模式下,GC有一個額外線程,能在運行時並發標記對象。

而由另一個線程去判斷是否壓縮對象,GC可以更傾向於決定不壓縮,有利於增強性能,但會增大應用程序工作集。使用並發垃圾回收器,消耗的內存比非並發更多。

加上以下配置項告訴CLR使用非並發模式:

<configuration>
  <runtime>
    <gcConcurrent enabled="false"/>
  </runtime>
</configuration>

使用需要特殊清理的類型

大多數類型只需要內存就可以了,然而有的類型還需要本機資源。比如System.IO.FileStream類型需要打開一個文件(本機資源)並保存文件的句柄。

包含本機資源的類型被GC時,GC會回收對象在托管堆中使用的內存。但這樣會造成本機資源(GC對它一無所知)的洩漏,所以CLR提供了稱為終結的機制,允許對象在被判定為垃圾之後,但在對象內存被回收之前執行一些代碼。

任何包裝了本機資源(文件,網絡連接,套接字,互斥體)的類型都支持終結。CLR判定一個對象不可達時,對象將終結它自己,釋放它包裝的本機資源。之後GC會從托管堆回收對象。

C#的語法,跟析構函數差不多,但是所代表的意義不同

 public class Troy {
        ~Troy() {
            //這裡的代碼就是垃圾回收前執行的代碼,這段代碼會被放在一個try塊中,而finally部分放的是base.Finalize
        }
    }

這個語法最後在IL代碼裡還是生成一個叫Finalize的方法。

被視為垃圾的對象在垃圾回收完畢後才調用Finalize方法,所以這些對象的內存不是馬上被回收,因為Finalize方法可能要執行訪問字段的代碼。

可終結對象在回收時必須存活,造成它被提升到另一代,使對象活得比正常時間長。這增大了內存耗用,所以應盡量避免終結。

終結的內部原理

在創建新對象的時候,會在堆中分配內存。如果對象的類型定義了Finalize方法,那麼在該類型的實例構造器被調用之前,會將指向該對象的指針放入到一個終結列表裡。

終結列表是一個由垃圾回收器控制的內部數據結構,列表的每一項都指向一個個對象——回收該對象的內存前應調用它的Finalize方法。

在每次要回收垃圾對象時標記階段走完都會去掃描終結列表,如果存在垃圾對象的引用,該引用被移除終結列表,並附加到freachable隊列。(此時對象將不再被認為是垃圾,不能回收其內存,被稱為對象復活了)

freachable隊列也是垃圾回收器的一個內部數據結構,隊列中的每個引用所指向的對象都已經准備好調用Finalize方法了。

CLR用一個特殊的、高優先級的專用線程調用Finalize方法來避免死鎖。

如果freachable隊列為空,那麼此線程睡眠,一旦不為空,此線程會被喚醒,將每一項都從隊列中移除,並且同時調用每個對象的Finalize方法。

然後進入壓縮階段,將這些復活的對象提升到下一代。

然後清空freachable隊列,並執行每個對象的Finalize方法。

到了下次執行垃圾回收時,因為終結列表已經沒有這些對象的指針了,所以現在它們被認為是真正的垃圾了,也就會被釋放。

整個過程中,執行了兩次垃圾回收才釋放掉內存,在實際的過程中,由於對象可能被提升至另一代,所以可能要求不止進行兩次垃圾回收。

手動監視和控制對象的生存期

CLR為每個AppDomain都提供了一個GC句柄表,允許應用程序監視和手動控制對象的生存期。這個就太6了,感覺用不到,用得到的時候回來再看吧。

 

PS:

最近兩章效率真是慢,一方面因為雙休沒看書和一些突發狀況,另一方面也是因為已經開始了CLR的核心機制之旅,裡面的很多東西確實沒聽過,感覺難度開始增大了。

在此過程中鍵盤莫名其妙壞了,並且兩次關機廢了寫了一半的博客。今天才發現原來強行關機後再開機,浏覽器中寫了一半的博客是可以恢復的。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved