託管堆和垃圾回收

1、託管堆基礎算法

1,訪問一個資源(文件、內存緩衝區、屏幕空間、網絡鏈接、數據庫資源等)所需的步驟數據庫

①調用IL指令newobj,爲表明資源的類型分配內存(通常使用c# new操做符來完成)c#

②初始化內存,設置資源的初始狀態並使資源可用。類型的實例構造器負責設置初始狀態數組

③訪問類型的成員來使用資源(有必要能夠重複)安全

④摧毀資源的狀態以進行清理服務器

⑤釋放內存。垃圾回收器獨自負責這一步網絡

 

2,從託管堆分配資源併發

初始化進程時,CLR劃出一個地址空間區域做爲託管堆,一個區域被非垃圾對象填滿後,CLR會分配更多的區域(32位進程最多能分配1.5GB,64爲進程最多能分配8TB)。CLR還要維護一個指針(NextObjPtr),該指針指向下一個對象在堆中的分配位值。剛開始的時候,NextObjPtr設爲地址空間區域的基地址。ide

 

3,C#的new操做符致使CLR執行如下步驟函數

①計算類型的字段(以及從基類型繼承的字段)所需的字節數。

②加上對象的開銷所需的字節數。每一個對象都有兩個開銷字段:類型對象指針和同步快索引(32位:兩個字段各需32位,因此每一個對象要增長8字節。64位:每一個字段各需64位,因此每一個對象要增長16字節)(int=4字節;long=8字節)

③CLR檢查區域中是否有分配對象所需的字節數。若是託管堆有足夠的可用空間,就在NextObjPtr指針指向的地址處放入對象,爲對象分配的字節會被清零。接着調用類型的構造器(爲this參數傳遞NextObjPtr),new操做符返回對象的引用。就在返回這個引用以前,NextObjPtr指針的值會加上對象佔用的字節數來獲得一個新值,即下一個對象放入托管堆是的地址

 

4,垃圾回收算法

CLR使用一種引用跟蹤算法。引用跟蹤算法只關心引用類型的變量,由於只有這種變量才能引用堆上的對象,咱們將全部引用類型的變量都稱爲

①CLR開始GC時,首先暫停進程中的全部線程(這樣能夠防止線程在CLR檢查期間訪問對象並更改其狀態)

②CLR進入GC標記階段(這個階段,CLR遍歷堆中全部對象,將同步塊索引字段中的一位設爲0。這代表全部的對象都應該刪除)

③CLR檢查全部活動根(根爲null,則CLR忽略這個根),查看他們引用了那些對象。若是引用了堆上的對象,CLR都會標記那個對象(將對象的同步塊索引中的位設置爲1)

④檢查完畢後,堆中的對象要麼標記。要麼未標記。已標記的對象不能被垃圾回收,由於至少有一個根在引用它,咱們說這些對象時可達

⑤進入GC的壓縮階段,在這個階段,CLR對堆中已標記的對象進行「乾坤大挪移」,壓縮全部倖存下來的對象,使它們佔用連續的內存對象

⑥壓縮以後,根如今的引用仍是原來的位置,而非移動以後的位置。因此做爲壓縮階段的一部分,CLR還要從每一個根減去所引用的對象在內存中的偏移的字節數。這樣就能保證根仍是引用和以前同樣的對象;只是對象在內存中換了位置

 

 

5,垃圾回收和調試

 

①使用Release編譯後,容許可執行文件,會發現TimerCallback方法只被調用了一次。由於Timer在初始化以後再也沒有用過變量t。(調試模式下Timer對象不會被回收)

        static void Main(string[] args)
        {
            //建立沒2000毫秒就調用一次TimerCallback方法的timer對象
            Timer t = new Timer(TimerCallback, null, 0, 2000);
            Console.ReadLine();
        }
        private static void TimerCallback(object o)
        {
            Console.WriteLine("a");

            //出於演示目的,強制執行一次垃圾回收
            GC.Collect();
        }

②顯示要求釋放計時器,它才能活到被釋放的那一刻

        static void Main(string[] args)
        {
            //建立沒2000毫秒就調用一次TimerCallback方法的timer對象
            Timer t = new Timer(TimerCallback, null, 0, 2000);
            Console.ReadLine();
            //在ReadLine以後引用t(在Dispose方法返回以前,t會在GC中存活)
            t.Dispose();
        }
        private static void TimerCallback(object o)
        {
            Console.WriteLine("a");

            //出於演示目的,強制執行一次垃圾回收
            GC.Collect();
        }

 

2、代:提高性能

對象越新,生存期越短
對象越老,生存期越長
回收堆的一部分,速度快於回收整個堆
1,原理
①CLR初始化堆時爲0代和1代選擇預算容量(以kb爲單位)。後期CLR會自動調節預算容量
②若是分配一個新的對象形成第0代超過預算,就必須啓動一次垃圾回收
③通過垃圾回收以後,第0代的倖存者被提高到1代(第一代的大小增長);第0代又空了出來
④因爲第0代已滿,因此必須垃圾回收。但這一次垃圾回收器發現第1代用完了預算容量。因此此次垃圾回收器決定檢查第1代和第0代的全部對象。兩代被垃圾回收之後,第1代的倖存者提高到2代,第0代的倖存者提高到1代

2,垃圾回收觸發的條件
①最多見觸發條件:CLR在檢查第0代超過預算時觸發一次GC
②代碼顯示調用Sytem.GC的靜態Collect方法
③Windows報告底內存狀況
④CLR正在卸載AppDomain
⑤CLR正在關閉(CLR在進程正常終止時)

3,大對象
目前認爲85000字節或更大的對象時大對象。(以前討論的都是小對象)。大對象通常是大字符串(好比XML或JSON)或者用於I/O操做的字節數組(好比從文件或網絡將字節讀入緩衝區一遍處理)
①大對象不是在小對象的地址空間分配,而是在進程地址空間的其餘地方分配
②目前版本的GC不壓縮大對象,由於在內存中移動它們的代價太高
③大對象老是第2代,毫不多是第0代或者第1代

4,垃圾回收模式

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

①兩個主要模式:

1>工做站

該模式針對客戶端應用程序優化GC。GC形成的延時很低,應用程序線程掛起時間很短,避免是用戶感到焦慮。

2>服務器

該模式針對服務器應用程序優化GC。被優化的主要是吞吐量和資源利用。

②應用程序模式以「工做站」GC模式運行

③顯示告訴CLR使用服務器回收站

  <runtime>
    <gcServer enabled="true"></gcServer>
  </runtime>
            //詢問CLR它是否在「服務器」GC模式中運行
            Console.WriteLine(GCSettings.IsServerGC);
            Console.ReadLine();

 ④兩個子模式(併發(默認)或非併發)

在併發模式中,垃圾回收器有一個額外的後臺線性,它能在應用程序運行時併發標記對象

  <runtime>
    <!--告訴CLR不要使用併發回收器-->
    <gcConcurrent enabled="false"></gcConcurrent>
  </runtime>

 ⑤GCSettings的LatencyMode屬性對垃圾回收進行某種程度的控制

符號名稱

說明

Batch(「服務器」GC模式的默認值)

關閉併發GC

Interactive(「工做站」GC模式的默認值)

打開併發GC

LowLatency

在短時間的、時間敏感的操做中(若是動畫繪製)使用這個延遲模式。這些操做不適合對第二代進行回收

Sustained LowLatency

使用這個延遲模式,應用程序的大多數操做都不會發生長的GC暫停。只要有足夠的內存,它將禁止全部會形成阻塞的第二代回收操做。事實上,這種應用程序(例如須要迅速響應的股票軟件)的用戶應該考慮安裝更多的RAM來防止發生生長的GC暫停

⑥正確的使用LowLatency

        static void Main(string[] args)
        {

            GCLatencyMode oldModel = GCSettings.LatencyMode;
            Console.WriteLine(oldModel);
            
            //約束執行區域(CER)
            System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                GCSettings.LatencyMode = GCLatencyMode.LowLatency;
                //在這裏運行你的代碼...
            }
            finally
            {
                GCSettings.LatencyMode = oldModel;
            }
            Console.ReadLine();

        }
View Code

 5,強制垃圾回收

public static void Collect(int generation, System.GCCollectionMode mode, bool blocking, bool compacting)

符號名稱

說明

Default

等同於不傳遞任何符號名稱。目前還等同於Forced,但將來的版本可能對此進行修改

Forced

強制回收指定的代(以及低於它的全部代)

Optimized

只有在能釋放大量內存或者能減小碎片化的前提下,才執行回收。若是垃圾回收沒有什麼效率,當前調用就沒有任何效果

若是寫一個CUI(控制檯用戶界面)或GUI(圖形用戶界面)應用程序,你可能但願建議垃圾回收的時間;爲此,請將GCCollectionMode設置爲Optimized並調用Collect。Default和Forced模式通常用於調試、測試和查找內存泄露

若是剛纔發生了某個非重複性的事件,並致使大量舊對象死亡,就可考慮手動調用一次collect方法。因爲是非重複性事件,垃圾回收器基於歷史的預測可能不許確。因此,這是調用collect方法時合適的

            //查看某一代發生了多少次垃圾回收
            Console.WriteLine(GC.CollectionCount(0));
            //查看託管堆中的對象當前使用了多少內存
            Console.WriteLine(GC.GetTotalMemory(true));

3、使用須要特殊清理的類型

包含本機資源的類型被GC時,GC會回收對象在託管堆中使用的內存。但這樣會形成本機資源的泄漏,因此CLR提供了稱爲終結的機制,容許對象在被斷定爲垃圾以後,但在對象內存被回收以前執行一些代碼。任何包裝了本機資源(文件、網絡鏈接、套接字、互斥體)的類型都支持終結。CLR斷定一個對象不可達時,對象將終結本身,釋放它包裝的本機資源。以後,GC會從託管堆回收對象

1,Finalize

它是爲釋放本機資源而設計的

    internal sealed class SomeType
    {
        //這是一個Finalize方法
        ~SomeType()
        {
            //這裏的代碼會進入Finalize方法
        }
    }

 2,SafeHandle

建立封裝了本機資源的託管類型是,應該先從using System.Runtime.InteropServices.SafeHandle這個特殊基類派生一個類

    public abstract class SafeHandle : CriticalFinalizerObject, IDisposable
    {
        //這是本機資源句柄
        protected IntPtr handle;

        protected SafeHandle(IntPtr invalidHandleValue, Boolean ownsHandle)
        {
            handle = invalidHandleValue;
            //若是ownsHandle爲true,那麼這個從SafeHandle派生的對象被回收時,本機資源會被關閉
        }

        protected SafeHandle(IntPtr invalidHandleValue)
        {
            handle = invalidHandleValue;
        }

        //顯式釋放資源
        public void Dispose(){Dispose(true);}

        //默認的Dispose實現(以下所示)正是咱們但願的。強烈建議不要重寫這個方法
        protected virtual void Dispose(Boolean disposing)
        {
            //這個默認實現會忽略disposing參數
            //若是資源已經釋放,那麼返回
            //若是ownsHandle爲true,那麼返回
            //設置一個標誌來指明該資源已經釋放
            //調用虛方法ReleaseHandle
            //調用GC.SuppressFinalize(this)方法來阻止調用Finalize方法
            //若是ReleaseHandled返回true,那麼返回
            //若是走到這一步,就激活ReleaseHandleFailed託管調試助手(MDA)

        }
        //派生類型要從寫這個方法以實現釋放資源的代碼
        protected abstract Boolean ReleaseHandle();

        //默認的Dispose實現(以下所示)正是咱們但願的。強烈建議不要重寫這個方法
        ~SafeHandle(){Dispose(false);}

        public void SetHandleAsInvalid()
        {
            //設置標誌來指出這個資源已經釋放
            //調用GC.SuppressFinalize(this)方法來阻止調用Finalize方法
        }

        public Boolean IsClosed {
            get { //返回指出資源是否釋放的一個標誌}
        }
        public abstract Boolean IsInvalid
        {
            //派生類要重寫這個屬性
            //若是句柄的值不表明資源(一般意味着句柄爲0或-1),實現應返回true
            get;
        }

        //如下三個方法設計安全性和引用計數
        public void DangerousAddRef(ref Boolean success){}
        public IntPtr DangerousGetHandle(){}
        public void DangerousRelease(){}

    }

CLR賦予這個類如下三個很酷的功能

①首次構造CriticalFinalizerObject派生類型對象時,CLR當即對繼承層次結構中的全部Finalize方法進行JIT編譯。構造對象時接編譯這些方法,可確保放當對象被肯定爲垃圾以後,本機資源確定會得以釋放。不對Finalize方法進行提早編譯,那麼也許能分配並使用本機資源,但沒法保證釋放。內存緊張時,CLR可能找不到足夠的內存來編譯Finalize方法,這會阻止Finalize方法的執行,形成本機資源泄漏。另外,若是Finalize方法中的代碼引用了另外一個程序集中的類型,但CLR定位該程序集失敗,那麼資源將得不到釋放。

②CLR是在調用了非CriticalFinalizerObject派生類型的Finalize方法以後,才調用CriticalFinalizerObject派生類的Finalize方法。這樣,託管資源類就能夠在它們的Finalize方法中成功地訪問CriticalFinalizerObject派生類型的對象。例如,FileStram類型的Finalize方法能夠放心地將數據從內存緩衝區flush到磁盤,它知道此時磁盤文件尚未關閉

③若是AppDomain被一個宿主應用程序(例如Microsoft SQL Server或者Microsoft ASP.NET)強行中斷,CLR將調用CriticalFinalizerObject派生類型的Finalize方法。宿主應用程序再也不信任它內部容許的託管代碼,也利用這個功能確保本機資源得以釋放。

3,SafeHandle派生類

SafeHandle派生類很是有用,由於它們保證本機資源在垃圾回收得以釋放

 

    internal static class SomeType
    {
        //這個原型不健壯
        [DllImport("Kernal32",CharSet = CharSet.Unicode,EntryPoint = "CreateEvent")]
        private static extern IntPtr CreateEventBad(IntPtr pSecurityAttribute, Boolean manualReset, Boolean initialState,
            string name);

        //這個原型是健壯的
        [DllImport("Kernal32", CharSet = CharSet.Unicode, EntryPoint = "CreateEvent")]
        private static  extern SafeWaitHandle CreateEventGood(IntPtr pSecurityAttribute, Boolean manualReset, Boolean initialState,
            string name);

        public static void SomeMethod()
        {
            IntPtr handle = CreateEventBad(IntPtr.Zero, false, false, null);
            SafeWaitHandle swh = CreateEventGood(IntPtr.Zero, false, false, null);
        }
    }
SomeType

 

4,使用包裝了本機資源的類型

1>以FileStream爲例,能夠用它打開一個文件,從文件中讀取字節,向文件中寫入字節,而後關閉文件
①FileStream對象在構造時會調用Win32 CreateFile函數
②函數返回句柄保存到SafeFileHandle對象中
③而後經過FileStream對象的一個私有字段來維護對象的引用

2>FileStream的Dispose方法

①FileStream實現了IDisposable接口。FileStream的Dispose方法會調用SafeFileHandle字段上的Dispose方法。
FileStream調用Dispose方法會清理本機資源。(並不是必定要調用Dispose才能保證本機資源得以清理。本機資源的清理最終總會發生,調用Dispose只是控制這個清理動做的發生時間)
FileStream調用Dispose方法不會致使FileStram對象從託管堆中刪除。只有在垃圾回收以後,託管堆中的內存纔會得以回收

        static void Main(string[] args)
        {
            //建立要寫入臨時文件的字節
            byte[] bytesToWrite = new byte[] {1, 2, 3, 4, 5};

            //建立臨時文件
            FileStream fs = new FileStream("Temp.dat", FileMode.Create);

            //將字節寫入臨時文件
            fs.Write(bytesToWrite, 0, bytesToWrite.Length);

            //刪除臨時文件
            File.Delete("Temp.dat");//拋出IOException異常

            Console.ReadLine();
        }
        static void Main(string[] args)
        {
            //建立要寫入臨時文件的字節
            byte[] bytesToWrite = new byte[] {1, 2, 3, 4, 5};

            //建立臨時文件
            FileStream fs = new FileStream("Temp.dat", FileMode.Create);

            //將字節寫入臨時文件
            fs.Write(bytesToWrite, 0, bytesToWrite.Length);
            
            //結束寫入後顯式關閉文件
            fs.Dispose();

            fs.Write(bytesToWrite, 0, bytesToWrite.Length);//拋出ObjectDisposedException

            //刪除臨時文件
            File.Delete("Temp.dat");//拋出IOException異常

            Console.ReadLine();
        }

 5,一個有趣的依賴性問題

            //建立臨時文件
            FileStream fs = new FileStream("Temp.txt", FileMode.Create);
            StreamWriter sw = new StreamWriter(fs);
            sw.Write("abc");
            //不要忘記這個Dispose的調用,不執行sw.Dispose()數據寫不進文件
            sw.Dispose();
            //注意:調用StreamWriter.Dispose會關閉FileStream;
            //FileStream對象無需顯示關閉

不須要再FileStream對象上顯式調用Dispose,由於StreanWrite會幫你調用。但若是非要顯式調用Dispose,FileStream會發現對象已經清理過了,因此方法什麼都不作直接返回

 6,終結器的內部工做原理

①應用程序建立新對象時,New操做符會從推中分配內存。若是對象的類型定義了Funalize方法,那麼在該類型的實例構造器被調用以前,會將指向該對象的指針放到一個終結列表

②垃圾回收時,對象B,D,E,F斷定爲垃圾。垃圾回收器掃描終結列表以查找這些對象的引用。找到一個引用以後,該引用從終結列表中移除,並附加到freachable隊列中

③一個特殊的高優先級CLR線程專門調用Finalize方法。一旦freachable隊列中有記錄項出現,線程就會喚醒,將每一項都從freachable隊列中移除,同時調用每一個對象的Finalize方法。

④下一次對老一代垃圾回收時,會發現已終結的對象成爲真正的垃圾,由於沒有應用程序的根指向它們,freachable隊列也再也不指向它們,因此,這些對象的內存會直接回收

(注意:CLR會忽略System.Object定義的Finalize方法)

(注意:可終結對象須要執行兩次垃圾回收才能釋放它們的內存。在實際應用中,因爲對象可能被提高至另外一代,因此可能要求不止進行兩次垃圾回收)

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

    public struct GCHandle
    {
        //靜態方法,用於在表中建立一個記錄項
        public static GCHandle Alloc(object value);
        public static GCHandle Alloc(object value,GCHandleType type);

        //靜態方法,用於將一個GCHandle轉成一個IntPtr
        public static explicit operator IntPtr(GCHandle value);
        public static IntPtr ToIntPtr(GCHandle value);

        //靜態方法,用於將一個IntPtr轉成一個GCHandle
        public static explicit operator GCHandle(IntPtr value);
        public static GCHandle FromIntPtr(IntPtr value);

        //實例方法,用於釋放表中的記錄項(索引設置爲0)
        public void Free();

        //實例屬性,用於get/set記錄項的對象引用
        public object Target { get; set; }

        //實例屬性,若是索引不爲0,就放回true
        public Boolean IsAllocated { get; }

        //對於已固定(pinned)的記錄項,這個方法返回對象的地址
        public IntPtr AddrOfPinnedObject();
    }

 

    public enum GCHandleType
    {
        Weak = 0, //監事對象的存在
        WeakTrackResurrection = 1, //監事對象的存在
        Normal = 2, //控制對象的生存期
        Pinned = 3 //控制對象的生存期
    }

Weak:

該標誌容許監視對象的生存期。可檢測垃圾回收器再何時斷定該對象在應用程序代碼中不可達。注意,此時對象的Finalize方法可能執行,也可能沒有執行,對象可能還在內存中。

WeakTrackResurrection:

該標誌容許監視對象的生存期。可檢測垃圾回收器在何時斷定該對象在應用程序的代碼不可達。注意,此時對象的Finalize方法(若是有的話)已經執行,對象的內存已經回收

Normal:

該標誌容許控制對象的生存期。告訴垃圾回收器:即便應用程序中沒有根引用對象,該對象也必須留在內存中。垃圾回收發生時,該對象的內存能夠壓縮(移動)。Alloc方法默認的標誌

Pinned: 

該標誌容許控制對象的生存期。告訴垃圾回收器:即便應用程序中沒有根引用對象,該對象也必須留在內存中。垃圾回收發生時,該對象的內存不壓縮(移動)。須要將內存地址交給本機代碼時,這個功能很好用。本機代碼知道GC不會移動對象,因此能放心地向託管堆的這個內存寫入。

1>垃圾回收器如何使用GC句柄表。當垃圾回收發生時,垃圾回收器的行爲以下①垃圾回收器標記全部可達的對象。而後。垃圾回收器掃描GC句柄表;全部Normal或Pinned對象都被當作是根,同時標記這些對象(包括對象經過他們的字段引用的對象)②垃圾回收器掃描GC句柄表,查找全部Weak記錄項。若是一個Weak記錄項引用了未標記的對象,該引用標識的就是不可達對象(垃圾),記錄項的引用值更改成null③垃圾回收器掃描終結列表。在列表中,對未標記對象的引用標識的是不可達對象,這個引用從終結列表移至freachable隊列,這是對象會被標記,由於對象又變成可達了④垃圾回收器掃描GC句柄表,查找全部WeakTrackResurrection記錄項。若是一個WeakTrackResurrection記錄項引用了未標記的對象(它如今是有freachable隊列中的記錄項引用的),該引用標識的就是不可達對象(垃圾),該記錄項的引用值更改成null⑤垃圾回收器對內存進行壓縮,填補不可達對象留下的內存「空調」,這其實就是一個內存碎片整理的過程。Pinned對象不會壓縮(移動),垃圾回收器會移動它周圍的其餘對象

相關文章
相關標籤/搜索