内存泄漏是指: 当一块内存被分配后, 被丢弃, 没有任何实例指针指向这块内存, 并且这块内存不会被 GC 视为垃圾进行回收这块内存会一直存在, 直到程序退出 C# 是托管型代码, 其内存的分配和释放都是由 CLR 负责, 当一块内存没有任何实例引用时, GC 会负责将其回收既然没有任何实例引用的内存会被 GC 回收, 那么内存泄漏是如何发生的?
为了演示内存泄漏是如何发生的, 我们来看一段代码
- 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 事件, 引发事件后, 会在屏幕上看到 TestAction Run 当 memory =null 后, memory 原来指向的内存就没有任何实例再引用该块内存了, 这样的内存就是待回收的内存 GC.Collect(GC.MaxGeneration) 语句会强制执行一次垃圾回收, 再次引发事件, 发现屏幕上还是会显示 TestAction Run 该内存没有被 GC 回收, 这就是内纯泄漏这是由 TestEvent+=memory.Run 语句引起的, 当 GC.Collect 执行的时候, 当他看到该块内存还有 TestEvent 引用, 就不会进行回收但是该内存已经是无法到达的了, 即无法调用该块内存, 只有在引发事件的时候, 才能执行该内存的 Run 方法这显然不是我想要的效果, 当 memory = null 执行时, 我希望该内存在 GC 执行时被回收, 并且当 TestEvent 被引发时, Run 方法不会执行, 因为我已经把该内存解放了
这里有一个问题, 就是 C# 中如何释放一块内存像 C 和 C++ 这样的语言, 内存的声明和释放都是开发人员负责的, 一旦内存 new 了出来, 就要 delete, 不然就会造成内存泄漏这更灵活, 也更麻烦, 一不小心就会泄漏, 忘记释放线程异常而没有执行释放的代码... 有手动分配内存的语言就有自动分配和释放的语言最开始使用垃圾回收的语言是 LISP, 之后被用在 Java 和 C# 等托管语言中像 C#,CLR 负责内存的释放, 当程序执行一段时间后, CLR 检测到垃圾内存已经值得进行一次垃圾回收时, 会执行垃圾回收至于如何判定一块内存是否为垃圾内存, 比较著名的是计数法, 即有一个实例引用了该内存后, 就在该内存的计数上 + 1, 改实例取消了对该内存的引用, 计数就 - 1, 当计数为 0 时, 就被判定为垃圾该种方法的问题是对循环引用束手无策, 如 A 的某个字段引用了 B, 而 B 的某个字段引用了 A, 这样 A 和 B 的技术都不会降到 0CLR 改用的方法是类似标记引用法 (我自己的命名): 在执行 GC 时, 会挂起全部线程, 并将托管堆中所有的内存都打上垃圾的标记, 之后遍历所有可到达的实例, 这些实例如果引用了托管堆的内存, 就将该内存的标记由垃圾变为被引用当遇到 A 和 B 相互引用的时候, 如果没有其他实例引用 A 或者 B, 虽然 A 和 B 相互引用, 但是 A 和 B 都是不可到达的, 即没办法引用 A 或者 B, 则 A 和 B 都会被判定为垃圾而被回收讲解了这么一大堆, 目的就是要说, 在 C# 中, 你想要释放一块内存, 你只要让该块内存没有任何实例引用他, 就可以了那么当执行 memory = null 后, 除了对 TestEvent 的订阅, 没有任何实例再引用了该块内存, 那么为什么订阅事件会阻止内存的释放?
我们来看看 TestEvent+=memory.Run() 这句话都干了什么我们利用 IL 反编译上面的 dll, 可以看到
- 1 IL_0000: nop
- 2 IL_0001: newobj instance void EventLeakMemory.Program/TestAction::.ctor()
- 3 IL_0006: stloc.0
- 4 IL_0007: ldloc.0
- 5 IL_0008: ldftn instance void EventLeakMemory.Program/TestAction::Run()
- 6 IL_000e: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
- 7 IL_0013: call void EventLeakMemory.Program::add_TestEvent(class [mscorlib]System.Action)
- ...// 其他部分
关键在 5-7 行第 5 和 6 行, 声明了一个 System.Action 型的委托, 参数为 TestAction.Run 方法, 第七行, 执行了 Program.add_TestEvent 方法, 参数是上面声明的委托也就是说 += 操作符相当于执行了 Add_TestEvent(new Action(memory.Run)), 就是这个 new Action 包含了对 memory 指向的内存的引用而这个引用在 CLR 看来是可达的, 可以通过引发事件来调用该内存
解决办法
我们已经找到了内存泄漏的元凶, 就是订阅事件时, 隐式声明的匿名委托对内存的引用最简单的解决办法是手动取消订阅事件, 只要 TestEvent -= memory.Run 就可以了但如何实现一个不需要手动取消订阅的事件? 该问题的解决办法是使用一种和普通的引用不同的方式来引用方法的实例对象: 该引用不会影响垃圾回收, 不会在 GC 时被判定为对该内存的引用, 也就是弱引用 C# 中, 绝大部分的类型都是强引用如何实现弱引用? 来看一个例子:
- 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();
- }
当执行 GCCollect 后, gcHandle.Target == null 由 false 变成了 true 这个 gcHandle 就是 obj 的一个弱引用这个类的详细介绍见 GCHandle 比较关键的是 GCHandle.Alloc 方法的第二个参数, 该参数接受一个枚举类型我使用的是 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>
- /// 引用的目标是否还存活 (没有被 GC 回收)
- /// </summary>
- public bool IsAlive {
- get {
- if (handle ==
- default(GCHandle)) return false;
- return handle.Target != null;
- }
- }
- /// <summary>
- /// 引用的目标
- /// </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>
- /// 引发事件
- /// </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);
- }
- }
来源: https://www.cnblogs.com/jazzpop/p/8656081.html