02-C#中的內(nèi)存管理

[TOC]

內(nèi)存管理

一、托管堆基礎(chǔ)

在面向?qū)ο笾?,每個類型代表一種可使用的資源,要使用該資源,必須為代表資源的類型分配內(nèi)存:

  • 調(diào)用IL指令newObj,為代表資源的類型分配內(nèi)存(一般使用new操作符完成);
  • 初始化內(nèi)存,設(shè)置資源的初始狀態(tài),并使資源可用。類型的實例構(gòu)造器完成該步驟;
  • 訪問類型的成員來使用資源;
  • 摧毀資源的狀態(tài)以進行清理;
  • 釋放內(nèi)存。由GC獨立完成。

一般只要是可驗證的、類型安全的代碼(不使用 unsafe 關(guān)鍵字),內(nèi)存一般不會被破壞。GC后仍可能出現(xiàn)內(nèi)存泄漏的情況:

  • 在集合中存儲了對象,但沒有按需移除對象;
  • 靜態(tài)字段引用某個集合對象,使集合一直存活,然后不停向集合中添加數(shù)據(jù)。

對于一般包裝了本機資源,如文件、套接字和數(shù)據(jù)庫連接等,的類型需要調(diào)用 Dispose 方法盡快手動清理,而不是等待GC接入。

1.1 從托管堆分配資源

托管堆與進程控件
進程初始化時,CLR劃分出一個地址控件作為托管堆,并要求所有的對象都要從托管堆分配。
內(nèi)用程序的內(nèi)存受進程的虛擬地址空間限制,32位進程最多能分配1.5GB,64位最多能分配8TB。
托管堆中現(xiàn)有控件被非垃圾對象填滿后,CLR會繼續(xù)分配更多區(qū)域,直到整個進程地址空間被填滿。

NextObjPtr指針
為了托管堆正常工作,CLR維護了一個指針,姑且命名為 NextObjPtr。它指向下一個對象在堆中的分配位置,初始時,該對象指向托管堆空間的基地址。

new操作符

  • 計算對象所需的字節(jié)數(shù);
  • CLR檢查區(qū)域空間并放入對象;
    對象的所需內(nèi)存包括以下兩部分:
  • 類型字段:自身包含的所有字段所需的內(nèi)存(自身定義 + 基類繼承);
  • 開銷字段:每個對象都存在兩個開銷字段,類型對象指針同步快索引;
    • 32位程序,兩個字段各需32位即4字節(jié),每個對象增加8字節(jié)空間。
    • 64位程序,兩個字段各需64位即8字節(jié),每個對象增加16字節(jié)空間。

CLR初始化對象有以下步驟:

  • CLR檢查托管堆中是否存在分配對象所需的字節(jié)數(shù);
  • 若足夠則在NextObjPtr當(dāng)前指向位置放入對象,初始化各字段內(nèi)存(若不夠則執(zhí)行垃圾回收);
  • 調(diào)用類型構(gòu)造器(為this參數(shù)傳遞 NextObjPtr),使用對象內(nèi)存更新指針位置,使指針指向下一個對象在堆中的位置;
  • new 操作符返回對象引用;

局部性原理
局部性原理:cpu訪問存儲器時,無論存取指令還是存取數(shù)據(jù),所訪問的存儲單元都趨于聚集在一個較小的連續(xù)趨于中。
托管堆的性能體現(xiàn):

  • 分配對象高速:只需要在指針上加一個新值即可;
  • 強關(guān)聯(lián)的對象連續(xù)分配:如在分配BinaryWriter之前需要分配一個FileStream,而BinaryWriter在內(nèi)部使用了FileStream;這些對象可以連續(xù)分配,并全部駐留在cpu緩存中;
  • CPU緩存數(shù)據(jù)可以以驚人的速度訪問,而不會因為 cache miss 導(dǎo)致訪問較慢的 RAM;

1.2 垃圾回收算法

1.2.1 傳統(tǒng)的引用計數(shù)算法

原理:堆上的每個對象都維護者一個內(nèi)存字段來統(tǒng)計程序中多少“部分”正在使用對象,當(dāng)某個對象不再使用該對象時,就遞減對象的引用計數(shù)字段,一旦該字段變成0,就從內(nèi)存中刪除該字段。
應(yīng)用:大多數(shù)系統(tǒng),如IOS、MOS、Microsoft的“組件對象模型”(Component Object Model, COM)等;
弊端:處理不好循環(huán)引用,通常需要加入其它的手段進行輔助控制,如弱引用;

1.2.2 引用跟蹤算法

原理:引用跟蹤算法只關(guān)心引用類型的變量,包括靜態(tài)的、實例的、方法參數(shù)和局部變量,所有的引用類型變量稱為根。過程為:

  • 準(zhǔn)備階段:CLR開始GC時,首先暫停進程中所有的線程,防止線程在CLR檢查期間更改對象的狀態(tài);
  • 標(biāo)記階段:
    • CLR將堆中所有對象的同步塊索引中的一位置為0,代表所有對象都應(yīng)該刪除;
    • CLR檢查所有活動根,若根存在引用的對象,則將被引用對象的同步塊索引中的位設(shè)為1,再檢查被標(biāo)記對象的所有根,標(biāo)記它們所引用的對象。
    • 若發(fā)現(xiàn)某個對象已被標(biāo)記,就不再檢查對象的字段,避免遇到循環(huán)引用,檢查陷入死循環(huán)。
    • 已標(biāo)記的對象說明至少有一個根在引用,稱為可達(reachable),不能被回收;
    • 未標(biāo)記的對象不存在使該對象能再次訪問的根,稱為不可達(unreachable),可被回收;
  • 壓縮(compact)階段:
    • 壓縮所有幸存的對象,使他們占用連續(xù)的內(nèi)存,恢復(fù)引用的“局部化”,減小了進程的工作集,提升了將來訪問這些對象的性能。
    • CLR從每個根減去所引用的對象再內(nèi)存中偏移的字節(jié)數(shù),使得對象能夠正確尋址訪問;
    • 托管堆的 NextObjPtr 指針指向最后一個幸存對象之后的位置;
      注意:若一次GC操作回收不到足夠的內(nèi)存,使得不足以再次分配新內(nèi)存后,使用new操作符會拋出 OutOfMemoryException 異常。

1.2.3 GC優(yōu)點

垃圾回收系統(tǒng)的好處有:

  • 無內(nèi)存泄漏;
  • 無內(nèi)存損壞;
  • 無地址控件碎片化;
  • 縮小進程工作集;
  • 同步線程;

說明:GC能作為線程同步機制來使用。由于GC會終結(jié)對象,所以可以知道所有線程都不再使用一個對象。

1.2.4 實例

public static class Program {
    public static void Main() {
        // 創(chuàng)建每2000ms就調(diào)用一次 TimerCallBack 方法的 Timer 對象
        Timer t = new Timer(TimerCallBack, null, 0, 2000);

        Console.ReadLine();

        // t = null; 無效代碼,這種情況下會被JIT編譯器優(yōu)化掉
    }
    private static void TimerCallback(Object o) {
        Console.WriteLine("In TimerCallback: " + DateTime.Now);

        // 強制執(zhí)行一次GC
        GC.Collect();
    }
}

現(xiàn)象:DEBUG模式下,timer會一直保持存活;RELEASE模式下,timer只調(diào)用了一次,在GC.Collect()中被強制回收了。
說明:在Debug下,JIT編譯器會強制將變量的生存期延長至方法結(jié)束,所以會一直運行。注意,由于GC提前調(diào)用,JIT編譯器會優(yōu)化掉 t = null; 之類的無效代碼;

二、代

代的工作假設(shè)前提:

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

GC代規(guī)則:

  1. GC為第0代設(shè)置一個預(yù)算容量,每次將不能回收的對象移動到第一代(第一次初始化第1代時,會為第1代分配一個預(yù)算空間);
  2. 當(dāng)?shù)?代滿時,會回收第1代,并將回收不了的對象移動到第2代(首次初始化第2代,會分配一個預(yù)算空間)。重復(fù)1、2步驟;
  3. 當(dāng)?shù)?代滿時,會檢查并回收第2代空間,若空間不足,則拋出OutOfMemoryException異常;

GC對每一代的空間預(yù)算是動態(tài)調(diào)節(jié)的,如GC發(fā)現(xiàn)每次回收0代后,存活對象很少,就可能減少第0代的預(yù)算。
但已分配空間的減少,會使GC更頻繁,但每次工作量也變少了,進程的工作集同時也減小了。
反之,每次回收0代后,存活對象多,則會增大預(yù)算。使得GC頻率減少,單次工作量大。
對于第1代和第2代也使用同樣的啟發(fā)式算法來動態(tài)調(diào)整內(nèi)存,根據(jù)App要求的內(nèi)存負載來自動優(yōu)化,提升App的整體性能。

public static class GCNotification {
    private static Action<Int32> s_gcDone = null;   // 事件字段

    public static event Action<Int32> GCDone {
        add {
            // 若之前沒有登記的委托,就開始報告通知
            if(s_gcDone == null) { new GenObject(0); new GenObject(2);}
            s_gcDone += value;
        }
        remove { s_gcDone -= value; }
    }

    private sealed class GenObject {
        private Int32 m_generation;
        public GenObject(Int32 generation) { m_generation = generation; }
        ~GenObject() { // 這是 Finalize 方法
            // 若這個對象在我們希望的或更高的代中,就通知委托一次GC剛剛完成
            if(GC.GetGeneration(this) >= m_generation) {
                Action<Int32> temp = Volatile.Read(ref s_gcDone);
                if(temp != null) temp(m_generation);
            }
            // 若至少還存在已登記的委托,且AppDomain并非正在卸載,進程并非正在關(guān)閉,就繼續(xù)報告通知
            if((s_gcDone != null)  
                && !AppDomain.CurrentDomain.IsFinalizingForUnload()
                && !Environment.HasShutdownStarted) {
                    // 對于第0代,創(chuàng)建一個新對象
                    // 對于第2代,復(fù)活對象,使第2代在下次回收時,GC會再次調(diào)用Finalize
                    if(m_generation == 0) new GenObject(0);
                    else GC.ReRegisterForFinalize(this);
                }
            else {
                /* 放過對象,讓其被回收 */
            }
        }
    }
}

2.1 垃圾回收觸發(fā)條件

  • CLR在檢測到第0代超出預(yù)算時觸發(fā)一次;
  • 顯示調(diào)用 System.GC.Collect 方法;
  • Windows 通過Win32函數(shù)檢測到內(nèi)存低時觸發(fā);
  • CLR 正在卸載 AppDomain 時(一個AppDomain卸載時,CLR認為其中一切都不是根,執(zhí)行一次涵蓋所有代的GC);
  • CLR 正在關(guān)閉。CLR在進程正常終止時,CLR認為進程中的一切都不是根。但此時CLR不會視圖壓縮或釋放內(nèi)存,由Windows回收進程的全部內(nèi)存。

2.2 大對象

大對象對于性能提升有很大影響。CLR將對象分為大對象和小對象,且以不同的方式對待他們。
CLR認為超過8500字節(jié)(約0.08M)或更大的對象是大對象。

  • 大對象不是在小對象的地址空間分配,而是在進程地址空間的其它地方分配;
  • 目前版本的GC不壓縮大對象,移動他們代價較高,但可能造成地址空間碎片化,導(dǎo)致拋出 OutOfMemoryException;
  • 大對象總是在第2代,絕不可能在第0代或第1代。分配短時間存活的大對象會導(dǎo)致第2代頻繁回收,損害性能;

大對象一般是大字符串(XML 或 JSON)或用于IO操作的字節(jié)數(shù)組;

關(guān)于大對象講解的幾篇博客
https://blog.csdn.net/jfkidear/article/details/18358551 大型對象堆揭秘
https://www.cnblogs.com/ygc369/p/4861610.html?utm_source=tuicool&utm_medium=referral 內(nèi)存管理優(yōu)化暢想
https://blog.csdn.net/cloudsuper/article/details/54924829 C#垃圾回收大對象

2.3 垃圾回收模式

CLR啟動時會選擇一個GC模式直到進程終止。存在兩個GC模式:

  • 工作站:針對客戶端應(yīng)用程序優(yōu)化GC,其特點有:
    • GC造成的延時低,線程掛起時間短;
    • 該模式中,GC假定機器上運行的其他應(yīng)用程序都不會消耗太多的CPU資源;
  • 服務(wù)器:針對服務(wù)器端應(yīng)用程序優(yōu)化,其特點有:
    • 主要優(yōu)化吞吐量和資源利用;
    • GC假定機器上沒有運行其他應(yīng)用程序,所有CPU都可以用來輔助完成GC;
    • 將托管堆拆分成幾個區(qū)域Section,每個CPU管理一個;
    • GC開始時,在每個CPU上都運行一個特殊線程,所有線程并發(fā)回收自己的區(qū)域;

GC默認以工作站模式運行,服務(wù)器應(yīng)用程序(ASP.NET 或 MS SQL SERVER)可請求CLR運行服務(wù)器模式;
如果服務(wù)器應(yīng)用程序運行在單核的機器上,CLR將總是使用“工作站”模式運行;

GC模式的設(shè)置和查詢
要設(shè)置GC運行模式,需要在應(yīng)用程序的配置文件中進行,在runtime中添加一個gcServer元素:

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

要查詢運行中的GC模式,調(diào)用GCSettings的IsServerGC屬性即可,為true為服務(wù)器模式運行:

bool isServerMode = GCSettings.IsServerGC;

子模式
除了“工作站”和“服務(wù)器”模式外,GC還支持兩種子模式:并發(fā)(默認)和非并發(fā)。
并發(fā)模式主指并發(fā)標(biāo)記對象,找到不可達對象集合。
在并發(fā)模式中,GC有一個額外的線程,在應(yīng)用程序運行時并發(fā)標(biāo)記對象。

一個線程因為分配對象造成第0代超出預(yù)算時,GC首先掛起所有線程,再判斷要回收哪些代。如果要回收第0代或第1代,那么一切正常進行。但是,如果要回收第2代,就會增大第0代的大?。ǔ^其預(yù)算),以便在第0代中分配新對象。然后,應(yīng)用程序的線程恢復(fù)運行。

并發(fā)模式下,垃圾回收器運行一個普通優(yōu)先級的后臺線程來查找不可達對象。不可達對象集合構(gòu)建好后,垃圾回收器會再次掛起所有線程,判斷是否要壓縮(移動)內(nèi)存:

  • 壓縮內(nèi)存:內(nèi)存會被壓縮,根引用會被修正,應(yīng)用程序線程恢復(fù)運行,該模式下,省去了查找不可達對象集合的時間;
  • 不壓縮內(nèi)存:若可用內(nèi)存多,GC更傾向于不壓縮內(nèi)存。有利于增強性能,但會增大工作集空間;

使用并發(fā)模式的垃圾回收器,應(yīng)用程序消耗的內(nèi)存通常比使用非并發(fā)垃圾回收器要多。
可以在runtime節(jié)點下,添加gcConcurrent元素來告訴CLR不使用并發(fā)回收器:

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

GC模式是針對進程配置的,進程運行期間不能更改。但可以使用GCSettings類對GCLatencyMode屬性對垃圾回收進行某種程度的控制。

Latency,潛在因素;潛伏的;延時

該屬性定義如下:

枚舉值名稱 說明
Batch("服務(wù)器"GC模式的默認值) 關(guān)閉并發(fā)GC
Interactive("工作站"GC模式默認值) 打開并發(fā)GC
LowLatency 低延時模式,多用于短期的、時間敏感、不適合對第2代進行回收的的操作,如動畫。
SustainedLowLatency 持續(xù)低延時模式。程序的大多數(shù)操作都不會發(fā)生長時間的GC暫停。

LowLatency 一般用于執(zhí)行一次低延時操作,執(zhí)行完畢后,再將模式設(shè)置回 Batch 或 Interactive。期間GC會全力避免回收第2代,除非調(diào)用GC.Collect或內(nèi)存低等必須回收第2代的操作。該模式中,程序拋出 OutOfMemoryException 的幾率較大。
注意事項:

  • 處于該模式的時間盡量短,避免分配太多對象,避免分配大對象;
  • 使用一個約束執(zhí)行區(qū)域(CER)將模式設(shè)回 Batch 或 Interactive;
  • 延遲模式是進程級設(shè)置,可能存在多個線程并發(fā)修改該設(shè)置,可使用線程同步鎖來操作該設(shè)置,如Interlocked更新計數(shù)器;

以下代碼展示如何正確地使用LowLatency模式:

private static void LowLatencyDemo() {
    GCLatencyMode oldMode = GCSettings.LatencyMode;
    System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions();
    try {
        GCSettings.LatencyMode = GCLatencyMode.LowLatency;
        // Do something in here
    } finally {
        GCSettings.LatencyMode = oldMode;
    }
}

2.4 強制垃圾回收

System.GC 類可對垃圾回收其進行一些直接控制:

  • GC.MaxGeneration:用來查詢托管堆中支持的最大代數(shù),該屬性總是返回2;
  • GC.Collect():強制對小于或等于指定代執(zhí)行垃圾回收,該方法最復(fù)雜的簽名如下:
/// <summary>強制對小于或等于指定代執(zhí)行垃圾回收<summary/>
/// <param>指定的代數(shù)</param>  
/// <param>回收模式</param>
/// <param>指定堵塞(非并發(fā))或后代(并發(fā))回收</param>
void Collect(Int32 generation, GCCollectionMode mode, Boolean blocking);

GCCollectionMode成員如下:

符號名稱 說明
Default 默認模式,強制回收指定代以及低于他的所有代
Forced 效果等同于Default,CLR未來版本可能對此進行優(yōu)化
Optimized 優(yōu)化模式,只有在能釋放大量內(nèi)存或減少碎片化的前提下才進行回收

對于GUI或CUI(Console User Interface)程序,應(yīng)用程序代碼將擁有進程和進程中的CLR,這里應(yīng)該將GCCollectionMode設(shè)置為Optimized。Default 和 Forced 一般用于調(diào)試、測試和查找內(nèi)存泄漏。
最好讓垃圾回收器依照自己的算法進行垃圾回收,根據(jù)程序的行為動態(tài)調(diào)整各個代的預(yù)算,避免手動調(diào)用Collect方法。

手動調(diào)用 GC.Collect 會導(dǎo)致代的預(yù)算發(fā)生調(diào)整,所以調(diào)用它不是為了改善應(yīng)用程序的響應(yīng)時間,而是為了減少進程工作集。如應(yīng)用程序初始化完成或用戶保存了一個數(shù)據(jù)文件之后,會導(dǎo)致大量的舊對象死亡,這里可以強制執(zhí)行一次GC。

對于內(nèi)存中存在大量對象的應(yīng)用程序,一次完全GC可能耗費很長時間,如服務(wù)器應(yīng)用程序。GC執(zhí)行時會掛起所有線程,會影響程序的正常工作,如客戶端請求超時。
GC類提供了一個RegisterForFullGCNotification方法,配合以下輔助方法:

  • WaitForFullGCApproach:和WaitForFullGCCompleted成對調(diào)用
  • WaitForFullGCCompleted:和WaitForFullGCApproach成對調(diào)用
  • CancelFullGCNotification

應(yīng)用程序就會在垃圾回收器將要執(zhí)行完全回收時收到通知,應(yīng)用程序就可以在更恰當(dāng)?shù)臅r候強制回收。

2.5 監(jiān)視應(yīng)用程序的內(nèi)存使用

可在進程中調(diào)用以下方法來監(jiān)視垃圾回收器:

  • Int32 CollectionCount(Int32 generation):查看某一代發(fā)生了多少次垃圾回收
  • Int64 GetTotalMemory(Boolean forecefullCollecton):托管堆中的對象當(dāng)前使用了多少內(nèi)存

為了評估(profile)特定代碼塊的性能,可以在代碼前后調(diào)用這些方法來計算差異??梢园盐沾a塊對進程工作集的影響,并了解執(zhí)行代碼塊時發(fā)生了多少次垃圾回收。

三、需要特殊清理的類型

大多數(shù)類型只要有內(nèi)存就能正常工作,但有的類型除了內(nèi)存還需要本機資源。
如:

  1. System.IO.FileStream 需要打開一個文件(本機資源)并保存文件的句柄,使用Read和Write方法用句柄操作文件;
  2. System.Threading.Mutex 類型打開一個 Windows 互斥體內(nèi)核對象(本機資源)并保存其句柄,調(diào)用Mutex方法時使用該句柄;

包含本資源的類型被GC時,GC會回收對象在托管堆中的內(nèi)存。但這樣會造成本機資源(GC對它一無所知)的泄漏,這是致命的問題。
CLR 提供了終結(jié)(finalization)的機制,允許對象在被判定為垃圾之后,但在對象內(nèi)存被回收之前執(zhí)行一些代碼。任何包裝了本機資源(文件、網(wǎng)絡(luò)連接、套接字、互斥體等)的類型都支持終結(jié)。CLR判定一個對象不可達時,對象將終結(jié)它自己,釋放它包裝的本機資源。之后,GC會從托管堆中回收對象。

System.Object 定義了受保護的虛方法 Finalize。垃圾回收器判定對象時垃圾后,會調(diào)用對象的Finalize方法(如果重寫)。Microsoft的C#團隊認為 Finalize 在編程語言中需要特殊的語法,類似于需要使用特殊語法定義構(gòu)造函數(shù) 。因此,C#要求在類名前添加 ~ 符號來定義Finalize方法,如下所示:

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

編譯以上代碼,用ILDasm.exe檢查的得到的程序集,會發(fā)現(xiàn)C#編譯器實際是在模塊的元數(shù)據(jù)中生成了名為 Finalize 的 protected override 方法。查看 Finalize 的 IL,會發(fā)現(xiàn)方法主體提的代碼被放到一個 try 塊中,在 finally 塊中則放入了一個 base.Finalize 調(diào)用。

Finalize 方法會延長不可達對象,以及該對象所引用的對象的生存周期;

可終結(jié)的對象在垃圾回收的最后階段,其Finalize方法被調(diào)用,由于Finalize方法要釋放資源,可能訪問對象中的字段,所以可終結(jié)對象在垃圾回收時必須存活,造成它被提升到另一代,以及字段所引用的對象也會被提升,這增大了內(nèi)存消耗,所以盡可能避免終結(jié)。

Finalize 方法的執(zhí)行時間是不確定的,應(yīng)用程序請求更多內(nèi)存時才有可能發(fā)生GC,而只有GC完成后才會運行Finalize方法,且CLR 不保證多個可終結(jié)對象的Finalize方法的調(diào)用順序,所以在Finalize方法中不要訪問其他可終結(jié)對象,因為這些對象可能已經(jīng)被終結(jié)。但可以安全地訪問值類型的實例或其他不可終結(jié)的對象。靜態(tài)方法中也可能訪問到已終結(jié)的對象,導(dǎo)致靜態(tài)方法的行為變得無法預(yù)測。

CLR使用一個專用的更高級的線程來調(diào)用Finalize方法,但是只要該線程被堵塞,應(yīng)用程序永遠無法調(diào)用其它對象的Finalize方法,使得對象無法被回收,造成內(nèi)存泄漏。若Finalize方法拋出異常,則進程終止,無法捕捉。

使用 Finalize 的問題較多,雖然他是為釋放本機資源而設(shè)計的,但是盡量不要手動去釋放他。

強烈建議不要重寫Object類的Finalze方法。相反,使用Microsoft在FCL中提供的輔助類。這些輔助類重寫了Finalize方法并添加了一些特殊的CLR魔法,可以從這些輔助類中派生出自己的類,從而繼承CLR的魔法。

創(chuàng)建包裝了本機資源的托管類型時,應(yīng)該先從 System.Runtime.InteropServices.SafeHandle 這個特殊基類派生出一個類(SafeHandle從名稱看出,安全句柄),該類的形式如下:

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

    protected SafeHandle(IntPtr invalidHandleValue, Boolean ownsHandle) {
        this.handle = invalidHandleValue;
        // 如果 ownsHandle 為 true,那么這個從 SafeHandle 派生的對象將被回收時,本機資源會關(guān)閉
    }

    protected void SetHandle(IntPtr handle) {
        this.handle = handle;
    }
    // 可調(diào)用 Dispose 顯式釋放資源,實現(xiàn)了 IDisposable 接口
    public void Dispose() { Dispose(true); }

    // 默認的Dispose實現(xiàn),強烈建議不要重寫該方法
    protected virtual void Dispose(Boolean disposing) {
        // 這個默認的實現(xiàn)會忽略 disposing 參數(shù);
        // 若資源已釋放,那么返回;
        // 若 ownsHandle 為 false, 那么返回;
        // 設(shè)置一個標(biāo)志來指明該資源已釋放;
        // 調(diào)用虛方法 ReleaseHandle;
        // 調(diào)用GC.SuppressFinalize(this)方法來阻止調(diào)用 Finalize 方法;
        // 如果 ReleaseHandle 返回 true,那么返回;
        // 如果執(zhí)行到這里,就激活 releaseHandleFailed 托管調(diào)試助手(MDA)
    }

    // 默認的 Finalize 實現(xiàn),強烈建議不要重寫這個方法。
    ~SafeHandle() { Dispose(false); }

    // 派生類需要重寫這個方法以實現(xiàn)釋放資源的代碼
    protected abstract Boolean ReleaseHandle();

    public void SetHandleAsInvalid() {
        // 設(shè)置標(biāo)志來指出這個資源已經(jīng)釋放
        // 調(diào)用GC.SuppressFinalize(this)方法來阻止調(diào)用Finalize方法
    }

    public Boolean IsClosed { get { /* 返回指出資源是否釋放的一個標(biāo)志 */ }}

    // 派生類要重寫這個屬性,如果句柄的值不代表資源(通常意味著句柄為0或-1),實現(xiàn)應(yīng)返回true  
    public abstract Boolean IsInvalid { get;}

    // 以下方法涉及安全性和引用計數(shù)
    public void DangerousAddRef(ref Boolean success) { ... }
    public IntPtr DangerousGetHandle() { ... }
    public void DangerousRelease() { ... }
}

SafeHandle 類有兩點需要注意:

  1. 派生自 CriticalFinalizerObject,其在 System.Runtime.ContrainedExecution 命名空間定義,CLR賦予這個類以下三個功能:
    • 構(gòu)造 CriticalFinalizerObject 對象時,CLR會立即對繼承層次中的所有Finalize方法JIT編譯,防止內(nèi)存緊張時,F(xiàn)inalize得不到編譯,以至于本機資源無法正常釋放;
    • CLR首先調(diào)用非 CriticalFinalizerObject 的 Finalize 方法,再調(diào)用派生類的 Finalize 方法,這樣,托管資源類就可以在它們的Finalize方法中成功地訪問 CriticalFinalizeObject 派生類型的對象。
    • 若 AppDomain 被一個宿主應(yīng)用程序(如SqlServer 或 Asp.Net)強行中斷,CLR將調(diào)用CriticalFinalizerObject派生類型的 Finalize 方法。宿主應(yīng)用程序不再信任它內(nèi)部運行的托管代碼時,也利用這個功能確保本機資源得以釋放。
  2. SafeHandle 是抽象類,必須有繼承類重寫受保護的構(gòu)造器、抽象方法 ReleaseHandle 以及抽象屬性 IsInvalid 的 get 訪問器方法。

構(gòu)造器不能虛或抽象,自然也不能重寫。重寫受保護的構(gòu)造器意思是說,派生類會定義個.ctor來調(diào)用受保護的.ctor,再重寫其他抽象成員。

SafeHandle 的派生類非常有用,它們能保證本機資源再垃圾回收時能夠得以釋放。
大多數(shù)本機資源都使用句柄(32位系統(tǒng)是32位值,64位系統(tǒng)是64位值)進行操作。所以SafeHandle類定義了受保護的 IntPtr 字段 handle。
在 Windows 中大多數(shù)值為0或-1的句柄都是無效的,所以Microsoft.Win32.SafeHandles命名空間包含繼承自SafeHandle的SafeHandleZeroOrMinusOneIsInvalid抽象輔助類,其結(jié)構(gòu)如下:

public abstract class SafeHandleZeroOrMinusOneIsInvalid : SafeHandle {
    protected SafeHandleZeroOrMinusOneIsInvalid(Boolean ownsHandle) 
        : base(IntPtr.Zero, ownsHandle) { }

    public override Boolean IsInvalid {
        get{
            if(base.handle == IntPtr.Zero) return true;
            if(base.handle == (IntPtr)(-1)) return true;
            return false;
        }
    }
}

要使用 SafeHandleZeroOrMinusOneIsInvalid 必須實現(xiàn)一個派生類,且“重寫”它受保護的構(gòu)造器和抽象方法 ReleaseHandle。.Net提供的派生類有:

  • SafeFileHandle;
  • SafeRegistryHandle;
  • SafeWaitHandle;
  • SafeMemoryMappedViewHandle;

其中 SafeFileHandle 類定義如下:

public sealed class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid {
    public SafeFileHandle(IntPtr preexistingHandle, Boolean ownsHandle)
        :base(ownsHandle) {
            base.SetHandle(preexistingHandle);
    }
    protected override Boolean ReleaseHandle() {
        // 告訴 Windows 希望關(guān)閉本機資源
        return Win32Native.CloseHandle(base.handle);
    }
}

其他幾種派生類的實現(xiàn)基本類似,其中SafeRegistryHandle類的ReleaseHandle方法調(diào)用的是 Win32 RegCloseKey 函數(shù)。
.Net之所以要提供這么多類,是要保證類型安全,禁止不同類型的句柄相互傳遞使用。

.Net 提供了很多額外的類型來包裝本機資源,如:SafeProcessHandle, SafeThreadHandle, SafeTokenHandle, SafeLibraryHandle以及SafeLocalAllocHandle等。這些類只在定義它們的程序集內(nèi)部使用,沒有公開。可能是微軟不想完整測試它們或不想花時間來編寫文檔。

SafeHandle第一個特性
與本機代碼互操作時,SafeHandle派生類將獲得CLR的特殊對待,如:

internal static class SomeType {
    [DllImport("Kernel32",CharSet=CharSet.Unicode, EntryPoint="CreateEvent")]
    private static extern IntPtr CreateEventBad(IntPtr pSecurityAttributes, 
                                                bool manualReset, 
                                                bool initialState, String name);

    [DllImport("Kernel32",CharSet=CharSet.Unicode, EntryPoint="CreateEvent")]
    private static extern SafeWaitHandle CreateEventGood(IntPtr pSecurityAttributes, 
                                                         bool manualReset, 
                                                         bool initialState, String name);

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

它們都調(diào)用了 CreateEvent 方法,該方法創(chuàng)建了一個本機事件資源,并將句柄返回。其中代碼

IntPtr handle = CreateEventBad(IntPtr.Zero, false, false, null);

其功能如下:

  • CreateEventBad 返回一個 IntPtr;
  • 將 IntPtr 賦值給一個 handle 變量。

這種方式代碼似乎沒問題,但以這種方式與本機代碼交互式不健壯的,在將句柄賦值給 handle 變量之前,可能會拋出一個 ThreadAbortException。雖然很少發(fā)生,但一旦發(fā)生,托管代碼將造成本機資源的泄漏,只能終止進程才能關(guān)閉事件。
SafeHandle 類修正了這個潛在的資源泄漏的問題。
CreateEventGood 方法返回一個 SafeWaitHandle,當(dāng) Win32 函數(shù) CreateEvent 返回至托管代碼時,CLR知道 SafeWaitHandle 是從 SafeHandle 派生的,所以會自動在托管堆構(gòu)造 SafeWaitHandle 的實例,向其傳遞 CreateEvent 返回的句柄值。
由于 SafeWaitHandle 對象的構(gòu)造以及句柄的賦值是在本機代碼中發(fā)生的,不可能被一個 ThreadAbortException 打斷,所以托管代碼不可能泄露這個本機資源。SafeWaitHandle 對象堆中會被垃圾回收,其Finalize方法會被調(diào)用,確保資源得以釋放。

SafeHandle第二個特性
本機資源使用的一個安全漏洞:

一個線程試圖使用一個本機資源,另一個線程視圖釋放該資源,這可能導(dǎo)致句柄循環(huán)使用漏洞。

SafeHandle 類防范這個安全隱患的辦法是使用引用計數(shù)。SafeHandle 類內(nèi)部定義了一個私有字段來維護一個計數(shù)器。一旦某個SafeHandle派生對象被設(shè)為有效句柄,計數(shù)器就被設(shè)為1。

  • 將 SafeHandle 派生對象作為實參傳給一個本機方法(非托管方法),CLR就會自動遞增計數(shù)器。
  • 當(dāng)本機方法返回到托管代碼時,CLR自動遞減計數(shù)器。
  • 計數(shù)器遞減為0,資源才會得以釋放。

Win32 的 SetEvent 函數(shù)原型如下:

[DllImport("Kernel32", ExactSpelling=true)]
private static extern Boolean SetEvent(SafeWaitHandle swh);

調(diào)用該方法并傳遞一個 SafeWaitHandle 對象的引用,CLR會在調(diào)用前遞增計數(shù)器,在調(diào)用后遞減計數(shù)器。對計數(shù)器的操作都是以線程安全的方式進行的。
若要將句柄作為一個 IntPtr 來操作,可以通過 SafeHandle 對象的 DangerousGetHandle 方法來返回原始句柄。但手動對原始句柄的訪問需要顯示操作引用計數(shù)器??赏ㄟ^ DangerousAddRef 和 DangerousRelease 方法來完成。

System.Runtime.InteropServices 還提供了一個 CriticalHandle 類。該類除了不提供引用計數(shù)外,其他方面和 SafeHandle 相同。CriticalHandle 類及其派生類通過犧牲安全性來換取性能(因為不使用操作計數(shù)器)。
CriticalHandle 也有提供了以下派生類:

  • CriticalHandleMinusOneIsInvalid;
  • CriticalHandleZeroOrMinusOneIsInvalid;

由于 Microsoft 傾向于建立更安全而不是更快的系統(tǒng),所有類庫中沒有提供從這兩個類派生的類型。使用時,建議權(quán)衡好安全性和性能之后來選擇 SafeHandle 或者 CriticalHandle。

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

System.IO.FileStream
FileStream在構(gòu)造時會調(diào)用win32的 CreateFile 函數(shù),該函數(shù)返回一個句柄保存在SafeFileHandle 類型的私有字段中。FileStream類還提供了Length,Position,CanRead等屬性,Read,Write,Flush等方法。
該類型的實現(xiàn)利用了一個內(nèi)存緩沖區(qū),只有緩沖區(qū)滿時,類型才將緩沖區(qū)中的數(shù)據(jù)刷入文件。

public void DemoMethod(){
    Byte[] bytesToWrite = new Byte[] { /* Some datas */ };
    // 創(chuàng)建臨時文件
    FileStream fs = new FileStream("temp.dat", FileMode.Create);
    // 將字節(jié)寫入
    fs.Write(bytesToWrite, 0, bytesToWrite.Length);
    // 刪除臨時文件
    File.Delete("temp.dat");    // 拋出 IOException 異常
}

對于 Delete 方法,絕大多數(shù)時候都會拋出IOException異常,因為此時文件沒有關(guān)閉。但CLR若湊巧在 Write 和 Delete 之間執(zhí)行了一次GC,那么FileStream的SafeFileHandle字段的Finalize方法會被調(diào)用,會釋放FileStream對象占用的本機資源,關(guān)閉文件,使Delete操作正常執(zhí)行。

3.1.1 Dispose說明

使用規(guī)范:若類的某個字段是實現(xiàn)了Dispose模式的類型,那么該類本身也應(yīng)該實現(xiàn)Dispose模式,并在Dispose方法中調(diào)用dispose字段的Dispose方法,來徹底釋放自身占用的資源;

實現(xiàn)了Dispose模式指實現(xiàn)了IDisposable接口;

通常所說“dispose一個對象”指的是:清理或處置對象以及它所引用的對象中包裝的資源,然后等待一次垃圾回收之后回收該對象占用的托管堆內(nèi)存(此時才釋放);
對于Dispose需要注意以下:

  • 并非一定要調(diào)用Dispose才能保證本機資源得以清理。本機資源的清理總會發(fā)生,調(diào)用Dispose方法只是控制這個清理動作的發(fā)生時間。
  • Dispose方法不會將托管對象從托管堆刪除,只有在垃圾回收之后,托管堆的內(nèi)存才會得以回收。
    FileStream實現(xiàn)了IDisposable接口,在實現(xiàn)方法中,在SafeFileHandle字段上調(diào)用了Dispose方法。在Write方法之后Delete方法之前,調(diào)用Dispose釋放掉本機資源,則文件可以正常刪除。

3.1.2 Dispose的使用

一般不應(yīng)該在代碼中顯示調(diào)用Dispose(確定需要清理資源時除外,如關(guān)閉打開的文件),GC知道一個對象何時不再被訪問,且只有到那個時候才會回收對象。而程序員很多時候并不清楚,如A將一個對象的引用傳給B,B將該對象的引用保存到自己的根中,而A并不知道對象已經(jīng)被B保存。此時A并不能明確能否調(diào)用該對象的Dispose,若關(guān)閉對象后,該對象的資源再被其它代碼訪問,則會造成拋出 ObjectDisposedException 。

Dispose()方法不是線程安全,也不應(yīng)該線程安全,代碼只有在確定沒有別的線程使用對象時,才應(yīng)調(diào)用Dispose。

對于Dispose的調(diào)用推薦使用以下寫法:

try{
    fs.Write(bytesToWrite, 0, bytesToWrite.Length);
} finally {
    if(fs != null) fs.Dispose();
}

該寫法等價于使用using關(guān)鍵字

using (FileStream fs = new FileStream("temp.dat", FileMode.Create)) {
    fs.Write(bytesToWrite, 0, bytesToWrite.Length);
}

using 語句只能用于那些實現(xiàn)了 IDisposable 接口的類型中。

3.2 一個依賴性問題

若沒有代碼顯示調(diào)用Dispose方法,則GC會在某個時刻檢測到對象時垃圾,并對它進行終結(jié)。但GC不保證對象的終結(jié)順序。

FileStream fs = new FileStream("DataFile.dat", FileMode.Create);
StreamWriter sw = new StreamWriter(fs);
sw.Write("Hello World");
sw.Dispose();

StreamWriter對象在寫入時,它會將數(shù)據(jù)緩存在自己的內(nèi)存緩沖區(qū)中。緩沖區(qū)滿時,StreamWriter對象會將數(shù)據(jù)寫入Stream對象中。
StreamWriter調(diào)用Dispose方法,會調(diào)用FileStreamDispose方法來關(guān)閉FileStream。StreamWriter終結(jié)后,會將數(shù)據(jù)Flush到FileStream中。Dispose工作交給GC來做,GC不能保證對象的終結(jié)順序,若先終結(jié)了FileStreamStreamWriter就會試圖向已關(guān)閉的文件中寫入數(shù)據(jù),造成異常。

Microsoft對這個依賴問題的解決方案是:

StreamWriter 類型不支持終結(jié),所有永遠不會將它的緩沖區(qū)中的數(shù)據(jù)flush到FileStream對象。這意味著若忘記在StreamWriter對象上顯式調(diào)用Dispose,則數(shù)據(jù)肯定會丟失。Microsoft希望開發(fā)人員注意到這個數(shù)據(jù)丟失問題,并插入對Dispose的調(diào)用來修正代碼。

3.3 GC為本機資源提供的其他功能

3.3.1 報告內(nèi)存壓力

本機資源有時會消耗大量內(nèi)存,但用于包裝它的托管對象只占用很少的內(nèi)存,如位圖。
一個位圖可能占用幾兆字節(jié)的本機內(nèi)存,但托管對象只包含一個 HBITMAP(4字節(jié)或8字節(jié))。
對CLR來說,在執(zhí)行下一次垃圾回收之前可能分配數(shù)百個位圖(極低內(nèi)存),但當(dāng)進程操作他們的時候,內(nèi)存消耗將猛增。
為了修正這個問題,GC類提供了兩個靜態(tài)方法:

public static void AddMemoryPressure(Int64 bytesAllocated);
public static void RemoveMemoryPressure(Int64 bytesAllocated);

可使用這些方法向垃圾回收器報告包裝很大的本機資源實際要消耗的內(nèi)存。垃圾回收器內(nèi)部就會監(jiān)視內(nèi)存壓力,適時進行回收。

static void Main(string[] args) {
    MemoryPressureDemo(0);                  // 0導(dǎo)致不頻繁的GC
    MemoryPressureDemo(10 * 1024 * 1024);   // 10MB 導(dǎo)致頻繁的GC
}

private static void MemoryPressureDemo(Int32 size)
{
    Console.WriteLine("\r\nMemoryPressureDemo, Size={0}", size);
    // 創(chuàng)建一組對象,并制定它們的邏輯大小
    for (int count = 0; count < 10; count++) {
        new BigNativeResource(size);
    }
    Console.WriteLine("Begin GC................");
    GC.Collect();   // 出于演示目的,強制執(zhí)行GC
}
// 占用指定內(nèi)存的本地資源
private sealed class BigNativeResource {
    private Int32 m_size;
    public BigNativeResource(Int32 size) {
        m_size = size;
        // 使垃圾回收期認為對象在物理上比較大
        if (m_size > 0) GC.AddMemoryPressure(m_size);
        Console.WriteLine("BigNativeResource create.({0})", m_size);
    }

    ~BigNativeResource() {
        //使垃圾回收期認為對象釋放了更多的內(nèi)存
        if (m_size > 0) GC.RemoveMemoryPressure(m_size);
        Console.WriteLine("BigNativeResource destroy.({0})",m_size);
    }
}

其可能的一次執(zhí)行結(jié)果如下:

MemoryPressureDemo, Size=0
BigNativeResource create.(0)
BigNativeResource create.(0)
BigNativeResource create.(0)
BigNativeResource create.(0)
BigNativeResource create.(0)
BigNativeResource create.(0)
BigNativeResource create.(0)
BigNativeResource create.(0)
BigNativeResource create.(0)
BigNativeResource create.(0)
Begin GC................

MemoryPressureDemo, Size=10485760
BigNativeResource create.(10485760)
BigNativeResource create.(10485760)
BigNativeResource create.(10485760)
BigNativeResource create.(10485760)
BigNativeResource create.(10485760)
BigNativeResource create.(10485760)
BigNativeResource create.(10485760)
BigNativeResource create.(10485760)
BigNativeResource destroy.(0)
BigNativeResource destroy.(0)
BigNativeResource destroy.(0)
BigNativeResource destroy.(0)
BigNativeResource destroy.(0)
BigNativeResource destroy.(0)
BigNativeResource destroy.(0)
BigNativeResource destroy.(0)
BigNativeResource create.(10485760)
BigNativeResource create.(10485760)
Begin GC................
BigNativeResource destroy.(0)
BigNativeResource destroy.(10485760)
BigNativeResource destroy.(0)
BigNativeResource destroy.(10485760)
BigNativeResource destroy.(10485760)
BigNativeResource destroy.(10485760)
BigNativeResource destroy.(10485760)
BigNativeResource destroy.(10485760)
BigNativeResource destroy.(10485760)
BigNativeResource destroy.(10485760)
BigNativeResource destroy.(10485760)
BigNativeResource destroy.(10485760)

3.3.2 限制允許資源個數(shù)

有的本機資源的數(shù)量是固定的且數(shù)量有限,一旦進程試圖使用超過允許數(shù)量的資源,通常會導(dǎo)致拋出異常。如以前Windows就限制只能創(chuàng)建5個設(shè)備上下文,應(yīng)用程序能打開的文件數(shù)量也必須有限制。
.Net使用了 System.Runtime.InteropServices.HandleCollector 類來解決這個問題:

public sealed class HandleCollector {
    public HandleCollector(String name, Int32 initialThreshold);
    public HandleCollector(String name, Int32 initialThreshold, Int32 maximumThreshold);
    public void Add();
    public void Remove();

    public Int32 Count { get; }
    public Int32 InitialThreshold { get; }
    public Int32 MaximumThreshold { get; }
    public String Name { get; }
}

如果要包裝超過 HandleCollector 限制的本機資源,就會被強制執(zhí)行垃圾回收。其演示代碼如下:

static void Main(string[] args) {
    HandleCollectorDemo();
}

private static void HandleCollectorDemo() {
    Console.WriteLine("\r\nHandleCollectorDemo");
    for (int count = 0; count < 10; count++) {
        new LimitedResource();
    }
    GC.Collect();   // 出于演示,強制一切都被清理
}

private sealed class LimitedResource {
    // 創(chuàng)建一個HandleCollector,告訴它當(dāng)兩個或更多這樣的對象存在于堆中的時候,就執(zhí)行回收
    private static readonly HandleCollector s_hc = new HandleCollector("LimitedResource", 2);

    public LimitedResource() {
        s_hc.Add(); // 告訴HandleCollector堆中增加了一個LimitedResource對象
        Console.WriteLine("LimitedResource create.Count={0}", s_hc.Count);
    }

    ~LimitedResource() {
        s_hc.Remove();  // 告訴HandleCollector堆中移除了一個LimitedResource對象
        Console.WriteLine("LimitedResource destroy.Count={0}", s_hc.Count);
    }
}

其可能的一次執(zhí)行結(jié)果如下:
HandleCollectorDemo
LimitedResource create.Count=1
LimitedResource create.Count=2
LimitedResource create.Count=3
LimitedResource destroy.Count=3
LimitedResource destroy.Count=2
LimitedResource destroy.Count=1
LimitedResource create.Count=1
LimitedResource create.Count=2
LimitedResource destroy.Count=2
LimitedResource create.Count=2
LimitedResource create.Count=3
LimitedResource destroy.Count=3
LimitedResource destroy.Count=2
LimitedResource destroy.Count=1
LimitedResource create.Count=1
LimitedResource create.Count=2
LimitedResource destroy.Count=2
LimitedResource create.Count=2
LimitedResource destroy.Count=1
LimitedResource destroy.Count=0

注意,在內(nèi)部,GC.AddMemoryPressure 和 HandleCollector.Add 方法都會調(diào)用 GC.Collect,在第0代超出預(yù)算前強制進行GC。這無疑會對性能造成負面影響。但是,性能受損總好過于本地資源用光,程序無法運行。

3.4 終結(jié)的內(nèi)部工作原理

終結(jié)表面上很簡單,創(chuàng)建對象,當(dāng)它被回收時,它的Finalize方法得以調(diào)用。
首先介紹幾個結(jié)構(gòu):

  • 終結(jié)列表:由GC控制的一個內(nèi)部數(shù)據(jù)結(jié)構(gòu),列表中的每一項都指向一個回收內(nèi)存前需要調(diào)用Finalize方法的對象;
  • freachable隊列:GC的一種內(nèi)部數(shù)據(jù)結(jié)構(gòu),隊列中的每個記錄項都引用著托管堆中一個已經(jīng)準(zhǔn)備好調(diào)用其Finalize方法的對象;

可終結(jié)的對象:從System.Object中繼承了Finalize方法,且將其重寫了,則認為該對象是“可終結(jié)的”,未重寫的Finalize方法會被CLR忽略,即便是從System.Object對象繼承。

終結(jié)的內(nèi)部過程:

  1. 創(chuàng)建新對象:調(diào)用new操作符,從堆中分配內(nèi)存。若該對象是可終結(jié)的,則該類型的實例構(gòu)造器被調(diào)用之前,CLR會將該對象的指針放到終結(jié)列表中。
  2. 開始垃圾回收:
    1. 未在終結(jié)列表,即非可終結(jié)的對象,被確定為垃圾后直接回收;
    2. 在終結(jié)列表中的垃圾對象,GC將其從終結(jié)列表移除,添加到freachable隊列中,此時可終結(jié)的垃圾對象依然在堆中(且依照GC算法,可能在當(dāng)前代,也可能移動到下一代中),因為Finalize方法還沒有調(diào)用,內(nèi)存不能回收;

freachable隊列增加了對可終結(jié)對象的引用,使對象從不可達變得重新可達,使得可終結(jié)對象被GC判定為垃圾后又變得不再是垃圾,即“復(fù)活”了

  1. 調(diào)用Finalize方法:一個專門線程將每一項都從freachable隊列中移除,同時調(diào)用每個對象的Finalize方法。由于該線程的特殊性,F(xiàn)inalize中的代碼不應(yīng)該對專用線程做出任何假設(shè),如不要在Finalize方法中訪問線程的本地存儲。

CLR專用線程:特殊的高優(yōu)先級的專門調(diào)用Finalize方法的線程。它可以避免潛在的線程同步問題。freachable隊列為空時,該線程睡眠,freachable存在記錄時則線程會被喚醒。
目前只有一個專用線程,可能調(diào)用代碼速度趕不上多核CPU分配可終結(jié)對象的速度,從而產(chǎn)生性能和伸縮性方面的問題。CLR未來可能使用多個終結(jié)器線程。

終結(jié)器的弊端:

  1. freachable對象會對不可達的對象重新標(biāo)記使其重新可達,標(biāo)記對象中的引用類型字段所引用的對象也必須復(fù)活;
  2. 可終結(jié)對象至少要執(zhí)行兩次垃圾回收才能釋放內(nèi)存,若對象被提升到另一代,可能遠不止兩次。

3.5 手動監(jiān)視和控制對象的生存期

CLR 為每個 AppDomain 都提供了一個 GC句柄表(GC Handle Table),允許應(yīng)用程序監(jiān)視或手動控制對象的生存期。該表在 AppDomain 創(chuàng)建之初是空白的。
表中每個記錄項包含兩種信息:

  • 對托管堆中的一個對象的引用。
  • 指明如何監(jiān)視或控制對象。

可使用 System.Runtime.InteropServices.GCHandle 結(jié)構(gòu)體在表中操作記錄項:

public struct GCHandle {
    // 用于在表中創(chuàng)建一個記錄項
    public static GCHandle Alloc(Object value);
    public static GCHandle Alloc(Object value, GCHandleType type);
    // 用于將一個 GCHandle 轉(zhuǎn)換成 IntPtr 
    public static explicit operator IntPtr(GCHandle value);
    public static IntPtr ToIntPtr(GCHandle value);
    // 用于將一個 IntPtr 轉(zhuǎn)換成 GCHandle
    public static explicit operator GCHandle(IntPtr value);
    public static GCHandle FromIntPtr(IntPtr value);
    // 用于比較兩個GCHandle
    public static Boolean operator ==(GCHandle a, GCHandle b);
    public static Boolean operator !=(GCHandle a, GCHandle b);

    public void Free();                 // 用于釋放表中的記錄項(索引設(shè)為0)
    public Object Target { get;set; }   // 用于引用記錄項中的對象
    public Boolean IsAllocated { get; } // 若索引不為0就返回true
    public IntPtr AddrOfPinnedObject(); // 對于已固定(pinned)的記錄項,返回對象的地址
}

為了監(jiān)視或控制對象的生命周期,可調(diào)用 GCHandle 的靜態(tài) Alloc 方法并傳遞目標(biāo)對象的引用。還可以傳入 GCHandleType 指定向如何監(jiān)視或控制對象,GCHandleType 枚舉類型定義如下:

public enum GCHandleType {
    // 0、1 允許監(jiān)視對象的生存期,它們都不可達??蓹z測出垃圾回收器判定該對象不可達的時間
    Weak = 0,                   // 此時對象還在內(nèi)存中,F(xiàn)inalize方法不確定是否執(zhí)行。
    WeakTrackResurrection = 1,  // (弱跟蹤復(fù)活)對象的內(nèi)存已回收,若存在Finalize方法,則已執(zhí)行
    // 2、3 允許控制對象的生存期,告訴垃圾回收器,即時該對象沒有被變量(根)引用,也必須留在內(nèi)存中
    Normal = 2,                 // 垃圾回收發(fā)生時,該對象的內(nèi)存可以壓縮(移動);
    Pinned = 3                  // 垃圾回收發(fā)生時,該對象的內(nèi)存不可以壓縮(移動)。
}

對于 Pinned 值,當(dāng)需要將內(nèi)存地址交給本機代碼時,這個功能很好用。本機代碼知道GC不會移動對象,所以能放心地向托管堆的這個內(nèi)存寫入。

GCHandle 的 Alloc 方法做了以下幾件事:

  • 掃描 AppDomain 的 GC 句柄表,查找一個可用的記錄項來存儲 Alloc 方法中傳入對象的引用;
  • 將句柄表中記錄項標(biāo)志設(shè)置為 GCHandleType 實參傳遞的值。
  • 返回一個 GCHandle 實例。

GCHandle 是輕量級的值類型,其中包含一個實例字段(一個IntPtr字段),它引用了句柄表中的記錄項的索引。要釋放 GC 句柄表中的這個記錄時可以獲取 GCHandle 實例,并在這個實例上調(diào)用 Free 方法。Free 方法將 IntPtr 字段設(shè)置為0,使實例變得無效。

當(dāng)垃圾回收發(fā)生時,垃圾回收器對 GC 句柄表的操作如下:

  1. 垃圾回收器標(biāo)記所有可達對象;
  2. 垃圾回收器掃描句柄表:
    1. 所有 Normal 和 Pinned 對象都被看成是根,并標(biāo)記這些對象以及它們所引用的對象;
    2. 查找所有 Weak 記錄項。
      • 若 Weak 記錄項引用了未標(biāo)記的對象,則該對象就是垃圾。將記錄項的引用值更改為null。
  3. 垃圾回收器掃描終結(jié)列表;
    • 將不可達對象從終結(jié)列表移至 freachable 隊列,這是對象會被標(biāo)記,重新“復(fù)活”變成可達;
  4. 垃圾回收器掃描GC句柄表,查找所有 WeakTrackResurrection 記錄項。
    • 若 WeakTrackResurrection 記錄項引用了未標(biāo)記的對象,則該對象就是垃圾。將記錄項的引用值更改為null。
  5. 垃圾回收器對內(nèi)存進行壓縮。Pinned 對象不會被壓縮。

標(biāo)記即為,垃圾回收器將對象的同步塊索引中的標(biāo)記位設(shè)置為1.

Normal 和 Pinned標(biāo)記
Normal 和 Pinned 通常在和本地代碼互操作時使用。
需要將托管代碼的指針移交給本機代碼時使用 Normal 標(biāo)記,因為本機代碼將來要回調(diào)托管代碼并傳遞指針。但不能直接將托管對象的指針交給本機代碼,因為如果垃圾回收發(fā)生,對象在內(nèi)存中移動,指針便無效了。
解決方案如下:

  • 調(diào)用 GCHandle 的 Alloc 方法,傳遞對象引用和 Normal 標(biāo)志。將返回的 GCHandle 實例轉(zhuǎn)型為 IntPtr,再將 IntPtr 傳給本機代碼。
  • 本機代碼回調(diào)托管代碼時,托管代碼將傳入的 IntPtr 轉(zhuǎn)型成 GCHandle,查詢 Target 屬性獲得托管對象的引用(當(dāng)前地址)。
  • 本機代碼不再需要這個引用之后,可以調(diào)用 GCHandle 的 Free 方法,使垃圾回收器能夠釋放對象。

這種情況下,本機代碼并沒有真正使用托管對象本身,它只是通過一種方式引用了對象。
但有時候本機代碼需要真正地使用托管對象本身,這時托管對象就必須要固定(Pinned)住,從而阻止垃圾回收器壓縮對象。

最常見的例子就是將托管的 String 對象傳給某個 Win32 函數(shù)。這時 String 對象必須固定。不能將托管對象的引用傳給本機代碼,若垃圾回收器在內(nèi)存中移動了對象,本機代碼就會向已經(jīng)不包含 String 對象的內(nèi)存進行讀寫,導(dǎo)致應(yīng)用程序的行為無法預(yù)測。

使用 CLR 的 P/Invoke 機制調(diào)用方法時,CLR 會自動幫你固定實參,并在本機方法返回時自動解除固定。

大多數(shù)時候都不需要使用 GCHandle 來顯示固定任何托管對象,只有在將托管對象指針傳給本機代碼,然后本機函數(shù)返回,但本機函數(shù)將來仍需要使用該對象時,才需要使用 GCHandle 類型。最常見的例子就是執(zhí)行異步 I/O 操作。

P/Invoke 的全稱是 Platform Invoke(平臺調(diào)用),實際上是一種函數(shù)調(diào)用機制,通過 P/Invoke 我們可以調(diào)用非托管的 DLL 中的函數(shù)。
P/Invoke 依次執(zhí)行以下操作:

  1. 查找包含該函數(shù)的非托管 DLL;
  2. 將該非托管 DLL 加載到內(nèi)存中;
  3. 查找函數(shù)在內(nèi)存中的地址并將其參數(shù)按照函數(shù)的調(diào)用約定壓棧;
  4. 將控制權(quán)轉(zhuǎn)移到非托管函數(shù);

GCHandle 實際使用示例:
假定分配了一個字節(jié)數(shù)組,并準(zhǔn)備在其中填充來自一個Socket的數(shù)據(jù),應(yīng)該如下操作:

  1. 調(diào)用GCHandle的Allc方法,傳遞數(shù)組對象的引用以及Pinned標(biāo)志;
  2. 在返回的 GCHandle 上調(diào)用 AddrOfPinnedObject 方法,返回已固定的對象在托管堆中的地址 IntPtr;
  3. 將該地址傳遞給本機函數(shù),該函數(shù)立即返回至托管代碼;
  4. 數(shù)據(jù)從 Socket 傳來時,由于設(shè)置了 Pinned,字節(jié)數(shù)組緩沖區(qū)在內(nèi)存中不會移動;
  5. 異步I/O操作完成后調(diào)用 GCHandle 的 Free 方法,之后垃圾回收器就可以移動緩沖區(qū)了;

托管代碼應(yīng)包含一個緩沖區(qū)的引用來訪問數(shù)據(jù),正式由于這個引用的存在,所以才會阻止垃圾回收從內(nèi)存中徹底釋放該緩沖區(qū)。

C# 提供了一個 fixed 語句,能夠在代碼塊中固定對象,使用示例如下:

unsafe public static void Go() {
    // 分配一系列立即編程垃圾的對象
    for (Int32 x = 0; x < 1000; x++) new Object();

    IntPtr originalMemoryAddress;
    Byte[] bytes = new Byte[1000];  // 在垃圾對象后分配這個數(shù)組

    // 獲取 Byte[] 在內(nèi)存中的地址
    fixed (Byte* pbytes = bytes) { originalMemoryAddress = (IntPtr) pbytes; }

    // 強迫進行一次垃圾回收:垃圾對象會被回收,Byte[] 可能被壓縮
    GC.Collect();
    // 獲取 Byte[] 當(dāng)前在內(nèi)存中的地址,把它同第一個地址比較
    fixed(Byte* pbytes = bytes) {
        Console.WriteLine("The Byte[] did{0} move during the GC", 
                    (originalMemoryAddress == (IntPtr)pbytes) ? "not" : null);
    }
}

使用 fixed 語句比分配一個固定 GC 句柄高效的多。
C# 編譯器在 pbytes 局部變量上生成一個特殊的“已固定”標(biāo)志。垃圾回收期間,GC 檢查這個根的內(nèi)容,如果根不為 null,就知道在壓縮期間不要移動變量引用的對象。C#編譯器生成IL將 pbytes 局部變量初始化為 fixed 塊起始處的對象的地址。在 fixed 塊的尾部,編譯器還會生成 IL 指令將 pbytes 局部變量設(shè)回 null,使變量不引用任何對象。這樣一來,下一次垃圾回收發(fā)生時,對象就可以移動了。

Weak 和 WeakTrackResurrection 標(biāo)記
它們既可以用于和本機代碼的互操作,也可以只在托管代碼的時候使用。

  • Weak:可以知道什么時候一個對象被判定為垃圾;
  • WeakTrackResurrection:可以知道什么時候?qū)ο蟮膬?nèi)存已經(jīng)被回收(極少使用)。

Weak 可以理解成傳統(tǒng)概念上的“弱引用”,其一般使用場景如下:
假定 Object-A 定時在 Object-B 上調(diào)用一個方法。但由于 Object-A 持有一個 對 Object-B 的引用,所以 Object-B 不會被垃圾回收。
在極少數(shù)情況下,可能有這樣的需求:
只要 Object-B 仍存活在托管堆中, Object-A 就能調(diào)用 Object-B 中的方法,就需要這樣做:

  1. Object-A 要調(diào)用 GCHandle 的 Alloc 方法,向方法中傳遞 Object-B 和 Weak 標(biāo)志;
  2. Object-A 需要持有 GCHandle 實例,而不是 Object-B 的引用;
    • Object-A 未持有 Object-B 的引用,若 Object-B 沒有其他根引用,就可以被回收。
  3. Object-A 想要調(diào)用 Object-B 的方法,需要查詢 GCHandle 的 Target 只讀屬性:
    1. 該屬性返回 null:Object-B 已被回收,Object-A 要調(diào)用 GCHandle 的 Free 方法來釋放他。
    2. 該屬性不為 null:Object-B 仍存活,將 Target 轉(zhuǎn)換為 Object-B 類型并調(diào)用方法。

使用 GCHandle 可以使一個對象“間接引用”另一個對象,但有些繁瑣,且要求提升的安全性才能在內(nèi)存中保持或固定對象。所以,System.WeakReference<T> 類對 GCHandle 使用了面向?qū)ο蟮陌b器進行了封裝,其基本結(jié)構(gòu)如下:

public sealed class WeakReference<T> : ISerializable where T : class {
    public WeakReference(T target);
    public WeakReference(T target, Boolean trackResurrection);
    public void SetTarget(T target);
    public Boolean TryGetTarget(out T target);
}

該類分析如下:

  • 構(gòu)造器:調(diào)用了 GCHandle 的 Alloc 方法;
  • SetTarget:設(shè)置 GCHandle 的 Target 屬性;
  • TryGetTarget:查詢 GCHandle 的 Target 屬性;
  • Finalize:以上未列出,調(diào)用了 GCHandle 的 Free 方法。

該類只支持弱引用,不支持 GCHandleType 值為 Normal 或 Pinned 的 GCHandle 實例的行為。WeakReference<T> 缺點在于它的實例必須在堆上分配,所以 WeakReference 類比 GCHandle 實例更“重”;

弱引用在緩存情形中能得到一定的應(yīng)用。可以若引用一些緩存對象來提升性能。但若對象被垃圾回收掉,再次需要這些對象時需要重新創(chuàng)建,程序的性能反而會收到壞影響。這就需要構(gòu)建良好的緩存算法來找到內(nèi)存消耗和速度之間的平衡點。
簡單來說,希望緩存保持對自己對象的強引用,一旦內(nèi)存緊張就開始將強引用轉(zhuǎn)換成弱引用。但目前 CLR 沒有提供內(nèi)存緊張的通知機制。但可以通過定時調(diào)用 Win32 GlobalMemoryStatusEx 函數(shù)并檢查返回的 MEMORYSTATUSEX 結(jié)構(gòu) dwMemoryLoad 成員值,若該值大于80,內(nèi)存空間就處于吃緊狀態(tài)。然后就可以將強引用轉(zhuǎn)換成若引用————可依據(jù)的算法包括:

  • 最近最少使用算法(Least-Recently Used algorithm, LRU);
  • 最頻繁使用算法(Most-Frequently Used algorithm, MFU);
  • 某個時基算法(Time-Base algorithm);

ConditionalWeakTable<TKey, TValue>
開發(fā)人員常需要將一些數(shù)據(jù)和另一個實體關(guān)聯(lián),如,數(shù)據(jù)可以和一個線程或 AppDomain 關(guān)聯(lián)??捎?System.Runtime.CompilerServices.ConditionalWeakTable<Tkey, TValue&gt 類將數(shù)據(jù)和單獨對象關(guān)聯(lián)。
該類使用方式與通常 Dictionary 字典類似,其結(jié)構(gòu)如下:

    public sealed class ConditionalWeakTable<TKey, TValue> where TKey : class 
                                                           where TValue : class
    {
        public ConditionalWeakTable();
        ~ConditionalWeakTable();
        public void Add(TKey key, TValue value);
        public TValue GetOrCreateValue(TKey key);
        public TValue GetValue(TKey key, CreateValueCallback createValueCallback);
        public bool Remove(TKey key);
        public bool TryGetValue(TKey key, out TValue value);

        public delegate TValue CreateValueCallback(TKey key);
    }

該類幾點說明如下:

  • 該類是線程安全的,也意味著它的性能并不出眾,使用時要確定他的性能是否適合實際生產(chǎn)環(huán)境;
  • 任意數(shù)據(jù)要和一個或多個對象關(guān)聯(lián),首先要創(chuàng)建該類的實例,調(diào)用 Add 方法為 Key 參數(shù)傳遞對象引用,為 value 參數(shù)傳遞想和對象關(guān)聯(lián)的數(shù)據(jù)。
  • 試圖多次添加對同一個對象的引用,Add方法會拋出 ArgumentException 異常;
  • 要修改和對象關(guān)聯(lián)的值,必須先刪除 key,再用新值把它添加回來。

ConditionalWeakTable 對象在內(nèi)存存儲了對作為 Key 的對象的弱引用。且還保證,只要 key 所標(biāo)識的對象在內(nèi)存中,那么對應(yīng)的 value 肯定在內(nèi)存中。這點是 ConditionalWeakTable 的核心功能;
ConditionalWeakTable 類可用于實現(xiàn) XAML 的依賴屬性機制。動態(tài)語言也可以在內(nèi)部利用它將數(shù)據(jù)和對象動態(tài)關(guān)聯(lián);

以下代碼延時了 ConditionalWeakTable 類的使用。它允許在任何對象上調(diào)用 GCWatch 擴展方法并傳遞一些 String 標(biāo)簽(在程序中作為通知消息顯示)。在特定對象被垃圾回收時,通過控制臺發(fā)出通知:

internal static class GCWatcher
{
    private readonly static ConditionalWeakTable<Object, NotifyWhenGCd<String>> s_cwt = new ConditionalWeakTable<object, NotifyWhenGCd<string>>();

    private sealed class NotifyWhenGCd<T>
    {
        private readonly T m_value;
        internal NotifyWhenGCd(T value) { m_value = value; }
        ~NotifyWhenGCd() { Console.WriteLine("GC'd: " + m_value); }
        public override string ToString() { return m_value.ToString(); }
    }

    public static T GCWatch<T>(this T @object, String tag) where T : class
    {
        s_cwt.Add(@object, new NotifyWhenGCd<string>(tag));
        return @object;
    }
}

使用示例如下:

static void Main(string[] args)
{
    Object o = null;
    new Object().GCWatch("My Object created at " + DateTime.Now);
    GC.Collect();       // 此時看不到 GC 通知
    GC.KeepAlive(o);    // 確定 o 引用的對象保持存活
    o = null;
    GC.Collect();       // 此時會看到GC通知
}

控制臺打印結(jié)果如下:

GC'd: My Object created at 2016/6/20 14:39:00
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容