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

[TOC]

內(nèi)存管理

一、托管堆基礎(chǔ)

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

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

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

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

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

1.1 從托管堆分配資源

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

NextObjPtr指針
為了托管堆正常工作纹安,CLR維護(hù)了一個指針尤辱,姑且命名為 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):

  • 分配對象高速:只需要在指針上加一個新值即可雷激;
  • 強(qiáng)關(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ù)算法

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

1.2.2 引用跟蹤算法

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

  • 準(zhǔn)備階段:CLR開始GC時舷暮,首先暫停進(jìn)程中所有的線程蟋座,防止線程在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)記的對象說明至少有一個根在引用会喝,稱為可達(dá)(reachable)陡叠,不能被回收;
    • 未標(biāo)記的對象不存在使該對象能再次訪問的根肢执,稱為不可達(dá)(unreachable)枉阵,可被回收;
  • 壓縮(compact)階段:
    • 壓縮所有幸存的對象预茄,使他們占用連續(xù)的內(nèi)存兴溜,恢復(fù)引用的“局部化”,減小了進(jìn)程的工作集耻陕,提升了將來訪問這些對象的性能拙徽。
    • CLR從每個根減去所引用的對象再內(nèi)存中偏移的字節(jié)數(shù),使得對象能夠正確尋址訪問诗宣;
    • 托管堆的 NextObjPtr 指針指向最后一個幸存對象之后的位置膘怕;
      注意:若一次GC操作回收不到足夠的內(nèi)存,使得不足以再次分配新內(nèi)存后梧田,使用new操作符會拋出 OutOfMemoryException 異常淳蔼。

1.2.3 GC優(yōu)點(diǎn)

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

  • 無內(nèi)存泄漏;
  • 無內(nèi)存損壞裁眯;
  • 無地址控件碎片化鹉梨;
  • 縮小進(jìn)程工作集;
  • 同步線程穿稳;

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

1.2.4 實(shí)例

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);

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

現(xiàn)象:DEBUG模式下骤菠,timer會一直保持存活;RELEASE模式下疤孕,timer只調(diào)用了一次商乎,在GC.Collect()中被強(qiáng)制回收了。
說明:在Debug下祭阀,JIT編譯器會強(qiáng)制將變量的生存期延長至方法結(jié)束鹉戚,所以會一直運(yùn)行。注意专控,由于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更頻繁权烧,但每次工作量也變少了眯亦,進(jìn)程的工作集同時也減小了。
反之般码,每次回收0代后妻率,存活對象多,則會增大預(yù)算板祝。使得GC頻率減少宫静,單次工作量大。
對于第1代和第2代也使用同樣的啟發(fā)式算法來動態(tài)調(diào)整內(nèi)存券时,根據(jù)App要求的內(nèi)存負(fù)載來自動優(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并非正在卸載震檩,進(jìn)程并非正在關(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認(rèn)為其中一切都不是根傻寂,執(zhí)行一次涵蓋所有代的GC)息尺;
  • CLR 正在關(guān)閉。CLR在進(jìn)程正常終止時疾掰,CLR認(rèn)為進(jìn)程中的一切都不是根搂誉。但此時CLR不會視圖壓縮或釋放內(nèi)存,由Windows回收進(jìn)程的全部內(nèi)存静檬。

2.2 大對象

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

  • 大對象不是在小對象的地址空間分配,而是在進(jìn)程地址空間的其它地方分配广恢;
  • 目前版本的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模式直到進(jìn)程終止趣惠。存在兩個GC模式:

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

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

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

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

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

bool isServerMode = GCSettings.IsServerGC;

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

一個線程因為分配對象造成第0代超出預(yù)算時,GC首先掛起所有線程就乓,再判斷要回收哪些代汉匙。如果要回收第0代或第1代拱烁,那么一切正常進(jìn)行。但是噩翠,如果要回收第2代戏自,就會增大第0代的大小(超過其預(yù)算)伤锚,以便在第0代中分配新對象擅笔。然后,應(yīng)用程序的線程恢復(fù)運(yùn)行屯援。

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

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

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

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

GC模式是針對進(jìn)程配置的羽戒,進(jìn)程運(yùn)行期間不能更改缤沦。但可以使用GCSettings類對GCLatencyMode屬性對垃圾回收進(jìn)行某種程度的控制。

Latency易稠,潛在因素;潛伏的缸废;延時

該屬性定義如下:

枚舉值名稱 說明
Batch("服務(wù)器"GC模式的默認(rèn)值) 關(guān)閉并發(fā)GC
Interactive("工作站"GC模式默認(rèn)值) 打開并發(fā)GC
LowLatency 低延時模式,多用于短期的驶社、時間敏感企量、不適合對第2代進(jìn)行回收的的操作,如動畫亡电。
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瓣赂;
  • 延遲模式是進(jìn)程級設(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 強(qiáng)制垃圾回收

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

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

GCCollectionMode成員如下:

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

對于GUI或CUI(Console User Interface)程序,應(yīng)用程序代碼將擁有進(jìn)程和進(jìn)程中的CLR栗弟,這里應(yīng)該將GCCollectionMode設(shè)置為Optimized污筷。Default 和 Forced 一般用于調(diào)試、測試和查找內(nèi)存泄漏乍赫。
最好讓垃圾回收器依照自己的算法進(jìn)行垃圾回收瓣蛀,根據(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)時間,而是為了減少進(jìn)程工作集改鲫。如應(yīng)用程序初始化完成或用戶保存了一個數(shù)據(jù)文件之后诈皿,會導(dǎo)致大量的舊對象死亡,這里可以強(qiáng)制執(zhí)行一次GC像棘。

對于內(nèi)存中存在大量對象的應(yīng)用程序纫塌,一次完全GC可能耗費(fèi)很長時間,如服務(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候強(qiáng)制回收。

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

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

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

為了評估(profile)特定代碼塊的性能凉逛,可以在代碼前后調(diào)用這些方法來計算差異性宏。可以把握代碼塊對進(jìn)程工作集的影響状飞,并了解執(zhí)行代碼塊時發(fā)生了多少次垃圾回收毫胜。

三、需要特殊清理的類型

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

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

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

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

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

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

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

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

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

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

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

強(qiáng)烈建議不要重寫Object類的Finalze方法柬赐。相反亡问,使用Microsoft在FCL中提供的輔助類。這些輔助類重寫了Finalize方法并添加了一些特殊的CLR魔法肛宋,可以從這些輔助類中派生出自己的類玛界,從而繼承CLR的魔法万矾。

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

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

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

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

    // 默認(rèn)的Dispose實(shí)現(xiàn),強(qiáng)烈建議不要重寫該方法
    protected virtual void Dispose(Boolean disposing) {
        // 這個默認(rèn)的實(shí)現(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)
    }

    // 默認(rèn)的 Finalize 實(shí)現(xiàn),強(qiáng)烈建議不要重寫這個方法询一。
    ~SafeHandle() { Dispose(false); }

    // 派生類需要重寫這個方法以實(shí)現(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),實(shí)現(xiàn)應(yīng)返回true  
    public abstract Boolean IsInvalid { get;}

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

SafeHandle 類有兩點(diǎn)需要注意:

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

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

SafeHandle 的派生類非常有用,它們能保證本機(jī)資源再垃圾回收時能夠得以釋放直砂。
大多數(shù)本機(jī)資源都使用句柄(32位系統(tǒng)是32位值菌仁,64位系統(tǒng)是64位值)進(jìn)行操作。所以SafeHandle類定義了受保護(hù)的 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 必須實(shí)現(xiàn)一個派生類洽蛀,且“重寫”它受保護(hù)的構(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)閉本機(jī)資源
        return Win32Native.CloseHandle(base.handle);
    }
}

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

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

SafeHandle第一個特性
與本機(jī)代碼互操作時靠抑,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)建了一個本機(jī)事件資源颂碧,并將句柄返回。其中代碼

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

其功能如下:

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

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

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

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

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

  • 將 SafeHandle 派生對象作為實(shí)參傳給一個本機(jī)方法(非托管方法)呛伴,CLR就會自動遞增計數(shù)器。
  • 當(dāng)本機(jī)方法返回到托管代碼時谒所,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ù)器的操作都是以線程安全的方式進(jìn)行的奕锌。
若要將句柄作為一個 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 使用包裝了本機(jī)資源的類型

System.IO.FileStream
FileStream在構(gòu)造時會調(diào)用win32的 CreateFile 函數(shù),該函數(shù)返回一個句柄保存在SafeFileHandle 類型的私有字段中罕模。FileStream類還提供了Length,Position,CanRead等屬性评腺,Read,Write,Flush等方法。
該類型的實(shí)現(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對象占用的本機(jī)資源,關(guān)閉文件全封,使Delete操作正常執(zhí)行马昙。

3.1.1 Dispose說明

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

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

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

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

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 語句只能用于那些實(shí)現(xiàn)了 IDisposable 接口的類型中考阱。

3.2 一個依賴性問題

若沒有代碼顯示調(diào)用Dispose方法翠忠,則GC會在某個時刻檢測到對象時垃圾,并對它進(jìn)行終結(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é),所有永遠(yuǎn)不會將它的緩沖區(qū)中的數(shù)據(jù)flush到FileStream對象划址。這意味著若忘記在StreamWriter對象上顯式調(diào)用Dispose扔嵌,則數(shù)據(jù)肯定會丟失限府。Microsoft希望開發(fā)人員注意到這個數(shù)據(jù)丟失問題,并插入對Dispose的調(diào)用來修正代碼痢缎。

3.3 GC為本機(jī)資源提供的其他功能

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

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

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

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

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();   // 出于演示目的遗遵,強(qiáng)制執(zhí)行GC
}
// 占用指定內(nèi)存的本地資源
private sealed class BigNativeResource {
    private Int32 m_size;
    public BigNativeResource(Int32 size) {
        m_size = size;
        // 使垃圾回收期認(rèn)為對象在物理上比較大
        if (m_size > 0) GC.AddMemoryPressure(m_size);
        Console.WriteLine("BigNativeResource create.({0})", m_size);
    }

    ~BigNativeResource() {
        //使垃圾回收期認(rèn)為對象釋放了更多的內(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ù)

有的本機(jī)資源的數(shù)量是固定的且數(shù)量有限,一旦進(jìn)程試圖使用超過允許數(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 限制的本機(jī)資源翼岁,就會被強(qiáng)制執(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();   // 出于演示司光,強(qiáng)制一切都被清理
}

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ù)算前強(qiáng)制進(jìn)行GC坞淮。這無疑會對性能造成負(fù)面影響谴仙。但是,性能受損總好過于本地資源用光碾盐,程序無法運(yùn)行晃跺。

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方法二打,且將其重寫了县忌,則認(rèn)為該對象是“可終結(jié)的”,未重寫的Finalize方法會被CLR忽略继效,即便是從System.Object對象繼承症杏。

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

  1. 創(chuàng)建新對象:調(diào)用new操作符,從堆中分配內(nèi)存瑞信。若該對象是可終結(jié)的厉颤,則該類型的實(shí)例構(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é)對象的引用振定,使對象從不可達(dá)變得重新可達(dá)必怜,使得可終結(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對象會對不可達(dá)的對象重新標(biāo)記使其重新可達(dá)类腮,標(biāo)記對象中的引用類型字段所引用的對象也必須復(fù)活;
  2. 可終結(jié)對象至少要執(zhí)行兩次垃圾回收才能釋放內(nèi)存蛉加,若對象被提升到另一代蚜枢,可能遠(yuǎn)不止兩次。

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)視對象的生存期,它們都不可達(dá)吃靠×蛘#可檢測出垃圾回收器判定該對象不可達(dá)的時間
    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)存地址交給本機(jī)代碼時骡澈,這個功能很好用锅纺。本機(jī)代碼知道GC不會移動對象,所以能放心地向托管堆的這個內(nèi)存寫入肋殴。

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

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

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

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

  1. 垃圾回收器標(biāo)記所有可達(dá)對象宴杀;
  2. 垃圾回收器掃描句柄表:
    1. 所有 Normal 和 Pinned 對象都被看成是根癣朗,并標(biāo)記這些對象以及它們所引用的對象;
    2. 查找所有 Weak 記錄項旺罢。
      • 若 Weak 記錄項引用了未標(biāo)記的對象旷余,則該對象就是垃圾。將記錄項的引用值更改為null扁达。
  3. 垃圾回收器掃描終結(jié)列表正卧;
    • 將不可達(dá)對象從終結(jié)列表移至 freachable 隊列,這是對象會被標(biāo)記跪解,重新“復(fù)活”變成可達(dá)炉旷;
  4. 垃圾回收器掃描GC句柄表,查找所有 WeakTrackResurrection 記錄項叉讥。
    • 若 WeakTrackResurrection 記錄項引用了未標(biāo)記的對象窘行,則該對象就是垃圾。將記錄項的引用值更改為null图仓。
  5. 垃圾回收器對內(nèi)存進(jìn)行壓縮罐盔。Pinned 對象不會被壓縮。

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

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

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

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

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

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

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

P/Invoke 的全稱是 Platform Invoke(平臺調(diào)用)卧须,實(shí)際上是一種函數(shù)調(diào)用機(jī)制另绩,通過 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 實(shí)際使用示例:
假定分配了一個字節(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. 將該地址傳遞給本機(jī)函數(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; }

    // 強(qiáng)迫進(jìn)行一次垃圾回收:垃圾對象會被回收缕粹,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)記
它們既可以用于和本機(jī)代碼的互操作情组,也可以只在托管代碼的時候使用燥筷。

  • 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 實(shí)例屈雄,而不是 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器進(jìn)行了封裝,其基本結(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 實(shí)例的行為视哑。WeakReference<T> 缺點(diǎn)在于它的實(shí)例必須在堆上分配绣否,所以 WeakReference 類比 GCHandle 實(shí)例更“重”誊涯;

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

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

ConditionalWeakTable<TKey, TValue>
開發(fā)人員常需要將一些數(shù)據(jù)和另一個實(shí)體關(guān)聯(lián)精耐,如狼速,數(shù)據(jù)可以和一個線程或 AppDomain 關(guān)聯(lián)。可用 System.Runtime.CompilerServices.ConditionalWeakTable<Tkey, TValue&gt 類將數(shù)據(jù)和單獨(dú)對象關(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);
    }

該類幾點(diǎn)說明如下:

  • 該類是線程安全的报咳,也意味著它的性能并不出眾,使用時要確定他的性能是否適合實(shí)際生產(chǎn)環(huán)境灰殴;
  • 任意數(shù)據(jù)要和一個或多個對象關(guān)聯(lián),首先要創(chuàng)建該類的實(shí)例,調(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)存中。這點(diǎn)是 ConditionalWeakTable 的核心功能承边;
ConditionalWeakTable 類可用于實(shí)現(xiàn) XAML 的依賴屬性機(jī)制遭殉。動態(tài)語言也可以在內(nèi)部利用它將數(shù)據(jù)和對象動態(tài)關(guān)聯(lián);

以下代碼延時了 ConditionalWeakTable 類的使用博助。它允許在任何對象上調(diào)用 GCWatch 擴(kuò)展方法并傳遞一些 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)系作者
  • 序言:七十年代末富岳,一起剝皮案震驚了整個濱河市蛔糯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌窖式,老刑警劉巖蚁飒,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異萝喘,居然都是意外死亡淮逻,警方通過查閱死者的電腦和手機(jī)琼懊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爬早,“玉大人肩碟,你說我怎么就攤上這事⊥勾唬” “怎么了削祈?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長脑漫。 經(jīng)常有香客問我髓抑,道長,這世上最難降的妖魔是什么优幸? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任吨拍,我火速辦了婚禮,結(jié)果婚禮上网杆,老公的妹妹穿的比我還像新娘羹饰。我一直安慰自己,他們只是感情好碳却,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布队秩。 她就那樣靜靜地躺著,像睡著了一般昼浦。 火紅的嫁衣襯著肌膚如雪馍资。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天关噪,我揣著相機(jī)與錄音鸟蟹,去河邊找鬼。 笑死使兔,一個胖子當(dāng)著我的面吹牛建钥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播虐沥,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼熊经,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了置蜀?” 一聲冷哼從身側(cè)響起奈搜,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盯荤,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體焕盟,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秋秤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年宏粤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片灼卢。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡绍哎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鞋真,到底是詐尸還是另有隱情崇堰,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布涩咖,位于F島的核電站海诲,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏檩互。R本人自食惡果不足惜特幔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望闸昨。 院中可真熱鬧蚯斯,春花似錦、人聲如沸饵较。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽循诉。三九已至撰茎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間打洼,已是汗流浹背龄糊。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留募疮,地道東北人炫惩。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像阿浓,于是被迫代替她去往敵國和親他嚷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345