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(); }
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); } }
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對象不會壓縮(移動),垃圾回收器會移動它周圍的其餘對象