溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

C#中event內(nèi)存泄漏的示例分析

發(fā)布時間:2021-09-03 15:12:58 來源:億速云 閱讀:235 作者:小新 欄目:編程語言

小編給大家分享一下C#中event內(nèi)存泄漏的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

內(nèi)存泄漏示例

為了演示內(nèi)存泄漏是如何發(fā)生的,我們來看一段代碼

class Program 
{
 static event Action TestEvent;
 static void Main(string[] args)
 {
  var memory = new TestAction();
  TestEvent += memory.Run;
  OnTestEvent();
  memory = null;
  //強制垃圾回收
  GC.Collect(GC.MaxGeneration);
  Console.WriteLine("GC.Collect");
  //測試是否回收成功
  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ā)事件后,會在屏幕上看到 TestAction Run。當(dāng)memory =null 后,memory原來指向的內(nèi)存就沒有任何實例再引用該塊內(nèi)存了,這樣的內(nèi)存就是待回收的內(nèi)存。GC.Collect(GC.MaxGeneration)語句會強制執(zhí)行一次垃圾回收,再次引發(fā)事件,發(fā)現(xiàn)屏幕上還是會顯示TestAction Run。該內(nèi)存沒有被GC回收,這就是內(nèi)純泄漏。這是由TestEvent+=memory.Run語句引起的,當(dāng)GC.Collect執(zhí)行的時候,當(dāng)他看到該塊內(nèi)存還有TestEvent引用,就不會進(jìn)行回收。但是該內(nèi)存已經(jīng)是“無法到達(dá)”的了,即無法調(diào)用該塊內(nèi)存,只有在引發(fā)事件的時候,才能執(zhí)行該內(nèi)存的Run方法。這顯然不是我想要的效果,當(dāng)memory = null執(zhí)行時,我希望該內(nèi)存在GC執(zhí)行時被回收,并且當(dāng)TestEvent被引發(fā)時,Run方法不會執(zhí)行,因為我已經(jīng)把該內(nèi)存“解放”了。

這里有一個問題,就是C#中如何“釋放”一塊內(nèi)存。像C和C++這樣的語言,內(nèi)存的聲明和釋放都是開發(fā)人員負(fù)責(zé)的,一旦內(nèi)存new了出來,就要delete,不然就會造成內(nèi)存泄漏。這更靈活,也更麻煩,一不小心就會泄漏,忘記釋放、線程異常而沒有執(zhí)行釋放的代碼...有手動分配內(nèi)存的語言就有自動分配和釋放的語言。最開始使用垃圾回收的語言是LISP,之后被用在Java和C#等托管語言中。像C#,CLR負(fù)責(zé)內(nèi)存的釋放,當(dāng)程序執(zhí)行一段時間后,CLR檢測到垃圾內(nèi)存已經(jīng)值得進(jìn)行一次垃圾回收時,會執(zhí)行垃圾回收。至于如何判定一塊內(nèi)存是否為垃圾內(nèi)存,比較著名的是計數(shù)法,即有一個實例引用了該內(nèi)存后,就在該內(nèi)存的計數(shù)上+1,改實例取消了對該內(nèi)存的引用,計數(shù)就-1,當(dāng)計數(shù)為0時,就被判定為垃圾。該種方法的問題是對循環(huán)引用束手無策,如A的某個字段引用了B,而B的某個字段引用了A,這樣A和B的技術(shù)都不會降到0。CLR改用的方法是類似“標(biāo)記引用法”(我自己的命名):在執(zhí)行GC時,會掛起全部線程,并將托管堆中所有的內(nèi)存都打上垃圾的標(biāo)記,之后遍歷所有可到達(dá)的實例,這些實例如果引用了托管堆的內(nèi)存,就將該內(nèi)存的標(biāo)記由垃圾變?yōu)楸灰?。?dāng)遇到A和B相互引用的時候,如果沒有其他實例引用A或者B,雖然A和B相互引用,但是A和B都是不可到達(dá)的,即沒辦法引用A或者B,則A和B都會被判定為垃圾而被回收。講解了這么一大堆,目的就是要說,在C#中,你想要釋放一塊內(nèi)存,你只要讓該塊內(nèi)存沒有任何實例引用他,就可以了。那么當(dāng)執(zhí)行memory = null后,除了對TestEvent的訂閱,沒有任何實例再引用了該塊內(nèi)存,那么為什么訂閱事件會阻止內(nè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行,聲明了一個System.Action型的委托,參數(shù)為TestAction.Run方法,第七行,執(zhí)行了Program.add_TestEvent方法,參數(shù)是上面聲明的委托。也就是說+=操作符相當(dāng)于執(zhí)行了Add_TestEvent(new Action(memory.Run)),就是這個new Action包含了對memory指向的內(nèi)存的引用。而這個引用在CLR看來是可達(dá)的,可以通過引發(fā)事件來調(diào)用該內(nèi)存。

解決辦法

我們已經(jīng)找到了內(nèi)存泄漏的元兇,就是訂閱事件時,隱式聲明的匿名委托對內(nèi)存的引用。該問題的解決辦法是使用一種和普通的引用不同的方式來引用方法的實例對象:該引用不會影響垃圾回收,不會在GC時被判定為對該內(nèi)存的引用,也就是“弱引用”。C#中,絕大部分的類型都是強引用。如何實現(xiàn)弱引用?來看一個例子:

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。這個gcHandle就是obj的一個弱引用。這個類的詳細(xì)介紹見 GCHandle 。比較關(guān)鍵的是GCHandle.Alloc方法的第二個參數(shù),該參數(shù)接受一個枚舉類型。我使用的是GCHandleType.Weak,表明該引用是個弱引用。利用這個方法,就可以封裝一個自己的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)是否還存活(沒有被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;
  }
 }
}

利用該類,就可以寫一個自己的弱事件封裝器。

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

以上是“C#中event內(nèi)存泄漏的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學(xué)習(xí)更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI