C#中event內(nèi)存泄漏總結(jié)
內(nèi)存泄漏是指:當(dāng)一塊內(nèi)存被分配后,被丟棄,沒(méi)有任何實(shí)例指針指向這塊內(nèi)存, 并且這塊內(nèi)存不會(huì)被GC視為垃圾進(jìn)行回收。這塊內(nèi)存會(huì)一直存在,直到程序退出。C#是托管型代碼,其內(nèi)存的分配和釋放都是由CLR負(fù)責(zé),當(dāng)一塊內(nèi)存沒(méi)有任何實(shí)例引用時(shí),GC會(huì)負(fù)責(zé)將其回收。既然沒(méi)有任何實(shí)例引用的內(nèi)存會(huì)被GC回收,那么內(nèi)存泄漏是如何發(fā)生的?
內(nèi)存泄漏示例
為了演示內(nèi)存泄漏是如何發(fā)生的,我們來(lái)看一段代碼
class Program { static event Action TestEvent; static void Main(string[] args) { var memory = new TestAction(); TestEvent += memory.Run; OnTestEvent(); memory = null; //強(qiáng)制垃圾回收 GC.Collect(GC.MaxGeneration); Console.WriteLine("GC.Collect"); //測(cè)試是否回收成功 OnTestEvent(); Console.ReadLine(); } public static void OnTestEvent() { if (TestEvent != null) TestEvent(); else Console.WriteLine("Test Event is null"); } class TestAction { public void Run() { Console.WriteLine("TestAction Run."); } } }
該例子中,memory.run訂閱了TestEvent事件,引發(fā)事件后,會(huì)在屏幕上看到 TestAction Run。當(dāng)memory =null 后,memory原來(lái)指向的內(nèi)存就沒(méi)有任何實(shí)例再引用該塊內(nèi)存了,這樣的內(nèi)存就是待回收的內(nèi)存。GC.Collect(GC.MaxGeneration)語(yǔ)句會(huì)強(qiáng)制執(zhí)行一次垃圾回收,再次引發(fā)事件,發(fā)現(xiàn)屏幕上還是會(huì)顯示TestAction Run。該內(nèi)存沒(méi)有被GC回收,這就是內(nèi)純泄漏。這是由TestEvent+=memory.Run語(yǔ)句引起的,當(dāng)GC.Collect執(zhí)行的時(shí)候,當(dāng)他看到該塊內(nèi)存還有TestEvent引用,就不會(huì)進(jìn)行回收。但是該內(nèi)存已經(jīng)是“無(wú)法到達(dá)”的了,即無(wú)法調(diào)用該塊內(nèi)存,只有在引發(fā)事件的時(shí)候,才能執(zhí)行該內(nèi)存的Run方法。這顯然不是我想要的效果,當(dāng)memory = null執(zhí)行時(shí),我希望該內(nèi)存在GC執(zhí)行時(shí)被回收,并且當(dāng)TestEvent被引發(fā)時(shí),Run方法不會(huì)執(zhí)行,因?yàn)槲乙呀?jīng)把該內(nèi)存“解放”了。
這里有一個(gè)問(wèn)題,就是C#中如何“釋放”一塊內(nèi)存。像C和C++這樣的語(yǔ)言,內(nèi)存的聲明和釋放都是開(kāi)發(fā)人員負(fù)責(zé)的,一旦內(nèi)存new了出來(lái),就要delete,不然就會(huì)造成內(nèi)存泄漏。這更靈活,也更麻煩,一不小心就會(huì)泄漏,忘記釋放、線程異常而沒(méi)有執(zhí)行釋放的代碼...有手動(dòng)分配內(nèi)存的語(yǔ)言就有自動(dòng)分配和釋放的語(yǔ)言。最開(kāi)始使用垃圾回收的語(yǔ)言是LISP,之后被用在Java和C#等托管語(yǔ)言中。像C#,CLR負(fù)責(zé)內(nèi)存的釋放,當(dāng)程序執(zhí)行一段時(shí)間后,CLR檢測(cè)到垃圾內(nèi)存已經(jīng)值得進(jìn)行一次垃圾回收時(shí),會(huì)執(zhí)行垃圾回收。至于如何判定一塊內(nèi)存是否為垃圾內(nèi)存,比較著名的是計(jì)數(shù)法,即有一個(gè)實(shí)例引用了該內(nèi)存后,就在該內(nèi)存的計(jì)數(shù)上+1,改實(shí)例取消了對(duì)該內(nèi)存的引用,計(jì)數(shù)就-1,當(dāng)計(jì)數(shù)為0時(shí),就被判定為垃圾。該種方法的問(wèn)題是對(duì)循環(huán)引用束手無(wú)策,如A的某個(gè)字段引用了B,而B(niǎo)的某個(gè)字段引用了A,這樣A和B的技術(shù)都不會(huì)降到0。CLR改用的方法是類似“標(biāo)記引用法”(我自己的命名):在執(zhí)行GC時(shí),會(huì)掛起全部線程,并將托管堆中所有的內(nèi)存都打上垃圾的標(biāo)記,之后遍歷所有可到達(dá)的實(shí)例,這些實(shí)例如果引用了托管堆的內(nèi)存,就將該內(nèi)存的標(biāo)記由垃圾變?yōu)楸灰?。?dāng)遇到A和B相互引用的時(shí)候,如果沒(méi)有其他實(shí)例引用A或者B,雖然A和B相互引用,但是A和B都是不可到達(dá)的,即沒(méi)辦法引用A或者B,則A和B都會(huì)被判定為垃圾而被回收。講解了這么一大堆,目的就是要說(shuō),在C#中,你想要釋放一塊內(nèi)存,你只要讓該塊內(nèi)存沒(méi)有任何實(shí)例引用他,就可以了。那么當(dāng)執(zhí)行memory = null后,除了對(duì)TestEvent的訂閱,沒(méi)有任何實(shí)例再引用了該塊內(nèi)存,那么為什么訂閱事件會(huì)阻止內(nèi)存的釋放?
我們來(lái)看看TestEvent+=memory.Run()這句話都干了什么。我們利用IL反編譯上面的dll,可以看到
IL_0000: nop IL_0001: newobj instance void EventLeakMemory.Program/TestAction::.ctor() IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: ldftn instance void EventLeakMemory.Program/TestAction::Run() IL_000e: newobj instance void [mscorlib]System.Action::.ctor(object, native int) IL_0013: call void EventLeakMemory.Program::add_TestEvent(class [mscorlib]System.Action)...//其他部分
關(guān)鍵在5-7行。第5和6行,聲明了一個(gè)System.Action型的委托,參數(shù)為TestAction.Run方法,第七行,執(zhí)行了Program.add_TestEvent方法,參數(shù)是上面聲明的委托。也就是說(shuō)+=操作符相當(dāng)于執(zhí)行了Add_TestEvent(new Action(memory.Run)),就是這個(gè)new Action包含了對(duì)memory指向的內(nèi)存的引用。而這個(gè)引用在CLR看來(lái)是可達(dá)的,可以通過(guò)引發(fā)事件來(lái)調(diào)用該內(nèi)存。
解決辦法
我們已經(jīng)找到了內(nèi)存泄漏的元兇,就是訂閱事件時(shí),隱式聲明的匿名委托對(duì)內(nèi)存的引用。該問(wèn)題的解決辦法是使用一種和普通的引用不同的方式來(lái)引用方法的實(shí)例對(duì)象:該引用不會(huì)影響垃圾回收,不會(huì)在GC時(shí)被判定為對(duì)該內(nèi)存的引用,也就是“弱引用”。C#中,絕大部分的類型都是強(qiáng)引用。如何實(shí)現(xiàn)弱引用?來(lái)看一個(gè)例子:
static void Main(string[] args){ var obj = new object(); var gcHandle = GCHandle.Alloc(obj, GCHandleType.Weak); Console.WriteLine("gcHandle.Target == null is :{0}", gcHandle.Target == null); obj = null; GC.Collect(); Console.WriteLine("GC.Collect"); Console.WriteLine("gcHandle.Target == null is :{0}", gcHandle.Target == null); Console.ReadLine(); }
當(dāng)執(zhí)行GC。Collect后,gcHandle.Target == null 由false 變成了true。這個(gè)gcHandle就是obj的一個(gè)弱引用。這個(gè)類的詳細(xì)介紹見(jiàn) GCHandle 。比較關(guān)鍵的是GCHandle.Alloc方法的第二個(gè)參數(shù),該參數(shù)接受一個(gè)枚舉類型。我使用的是GCHandleType.Weak,表明該引用是個(gè)弱引用。利用這個(gè)方法,就可以封裝一個(gè)自己的WeakReference類,代碼如下
public class WeakReference<T> where T : class { private GCHandle handle; public WeakReference(T obj) { if (obj == null) return; handle = GCHandle.Alloc(obj, GCHandleType.Weak); } /// <summary> /// 引用的目標(biāo)是否還存活(沒(méi)有被GC回收) /// </summary> public bool IsAlive { get { if (handle == default(GCHandle)) return false; return handle.Target != null; } } /// <summary> /// 引用的目標(biāo) /// </summary> public T Target { get { if (handle == default(GCHandle)) return null; return (T)handle.Target; } } }
利用該類,就可以寫(xiě)一個(gè)自己的弱事件封裝器。
public class WeakEventManager<T> { private Dictionary<Delegate, WeakReference<T>> delegateDictionary; public WeakEventManager() { delegateDictionary = new Dictionary<Delegate, WeakReference<T>>(); } /// <summary> /// 訂閱 /// </summary> public void AddHandler(Delegate handler) { if (handler != null) delegateDictionary[handler] = new WeakReference<T>(handler); } /// <summary> /// 取消訂閱 /// </summary> public void RemoveHandler(Delegate handler) { if (handler != null) delegateDictionary.Remove(handler); } /// <summary> /// 引發(fā)事件 /// </summary> public void Raise(object sender, EventArgs e) { foreach (var key in delegateDictionary.Keys) { if (delegateDictionary[key].IsAlive) key.DynamicInvoke(sender, e); else delegateDictionary.Remove(key); } } }
最后,就可以像下面這樣定義自己的事件了
public class TestEventClass { private WeakEventManager<Action<object, EventArgs>> _testEvent = new WeakEventManager<Action<object, EventArgs>>(); public event Action<object, EventArgs> TestEvent { add { _testEvent.AddHandler(value); } remove { _testEvent.RemoveHandler(value); } } protected virtual void OnEvent(EventArgs e) { _testEvent.Raise(this, e); } }
相關(guān)文章
C#實(shí)現(xiàn)圖像選擇驗(yàn)證碼的示例代碼
為了防止網(wǎng)站被非法登陸,網(wǎng)站一般通過(guò)驗(yàn)證碼的方式,防止黑客用軟件非法登陸,本文主要介紹了C#實(shí)現(xiàn)圖像選擇驗(yàn)證碼的示例代碼,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08C#實(shí)現(xiàn)獲取鼠標(biāo)句柄的方法
這篇文章主要介紹了C#實(shí)現(xiàn)獲取鼠標(biāo)句柄的方法,詳細(xì)的講述了實(shí)現(xiàn)獲取鼠標(biāo)句柄的具體步驟及實(shí)現(xiàn)方法,并附有完整的實(shí)例源碼供大家參考,需要的朋友可以參考下2014-09-09C# Dynamic之:ExpandoObject,DynamicObject,DynamicMetaOb的應(yīng)用(下)
本篇文章是對(duì)C#中ExpandoObject,DynamicObject,DynamicMetaOb的應(yīng)用進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05Visual Studio連接unity編輯器的實(shí)現(xiàn)步驟
unity編輯器中打開(kāi)C#腳本的時(shí)候發(fā)現(xiàn)Visual Studio沒(méi)有連接unity編輯器,本文主要介紹了Visual Studio連接unity編輯器的實(shí)現(xiàn)步驟,感興趣的可以了解一下2023-11-11C#把EXCEL數(shù)據(jù)轉(zhuǎn)換成DataTable
這篇文章介紹了C#把EXCEL數(shù)據(jù)轉(zhuǎn)換成DataTable的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-04-04C# Winform程序?qū)崿F(xiàn)防止多開(kāi)的方法總結(jié)【親測(cè)】
這篇文章主要介紹了C# Winform程序?qū)崿F(xiàn)防止多開(kāi)的方法,結(jié)合實(shí)例形式總結(jié)分析了C# Winform防止多開(kāi)相關(guān)操作技巧與使用注意事項(xiàng),需要的朋友可以參考下2020-03-03C#使用SQLite進(jìn)行大數(shù)據(jù)量高效處理的代碼示例
在軟件開(kāi)發(fā)中,高效處理大數(shù)據(jù)量是一個(gè)常見(jiàn)且具有挑戰(zhàn)性的任務(wù),SQLite因其零配置、嵌入式、跨平臺(tái)的特性,成為許多開(kāi)發(fā)者的首選數(shù)據(jù)庫(kù),本文將深入探討如何使用SQLite優(yōu)化大數(shù)據(jù)量的存儲(chǔ)和檢索,,需要的朋友可以參考下2025-04-04