C#内存泄漏:Event内存泄漏

C#常见内存泄漏 在开发中由于对语言特性不了解或经验不足或疏忽,往往会造成一些低级bug。而内存泄漏就是最常见的一个,这个问题在测试过程中,因为操作频次低,而不能完全被暴露出来;而在正式使用时,由于使用次数增加,这个问题在很快就会出现。一旦出现就会导致程序直接退出或报错……使用中得益于使用量的增加,未被回收的小对象不断实例化,数量的叠加,导致内存使用率会随时间的增长而增加,直到影响程序的正常执行。为了警醒鄙人,同时方便以后查阅,将在项目中实际处理的内存泄漏情况与处理办法进行下述总结。 阅读详情

前言

内存泄漏是指:当一块内存被分配后,被丢弃,没有任何实例指针指向这块内存, 并且这块内存不会被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的技术都不会降到0。

CLR改用的方法是类似“标记引用法”(我自己的命名):在执行GC时,会挂起全部线程,并将托管堆中所有的内存都打上垃圾的标记,之后遍历所有可到达的实例,这些实例如果引用了托管堆的内存,就将该内存的标记由垃圾变为被引用。

当遇到A和B相互引用的时候,如果没有其他实例引用A或者B,虽然A和B相互引用,但是A和B都是不可到达的,即没办法引用A或者B,则A和B都会被判定为垃圾而被回收。讲解了这么一大堆,目的就是要说,在C#中,你想要释放一块内存,你只要让该块内存没有任何实例引用他,就可以了。

那么当执行memory = null后,除了对TestEvent的订阅,没有任何实例再引用了该块内存,那么为什么订阅事件会阻止内存的释放?

我们来看看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)
...//其他部分

关键在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();
}

当执行GC。Collect后,gcHandle.Target == null 由false 变成了true。

这个gcHandle就是obj的一个弱引用。这个类的详细介绍见 GCHandle 。

比较关键的是GCHandle.Alloc方法的第二个参数,该参数接受一个枚举类型。

我使用的是GCHandleType.Weak,表明该引用是个弱引用。利用这个方法,就可以封装一个自己的WeakReference类,代码如下:


public class WeakReference<TDelegate> : IEquatable<Delegate> {
    private GCHandle _handle;
    public WeakReference(Delegate obj) {
        if (obj == null) return;
        _handle = GCHandle.Alloc(obj, GCHandleType.Weak);
    }
    /// <summary>
   /// 引用的目标是否还存活(没有被GC回收)
    /// </summary>
    public bool IsAlive {
        get { return _handle != default(GCHandle) && _handle.Target != null; }
    }

    /// <summary>
    /// 引用的目标
    /// </summary>
    public TDelegate Target {
        get {
            if (_handle == default(GCHandle)) return default(TDelegate);
            return (TDelegate)_handle.Target;
        }
    }

    /// <summary>
    /// 实现接口,方便与委托的比较
    /// </summary>
    /// <param name="other"></param>
    /// <returns></returns>
    public bool Equals(Delegate other) {
        return _handle != default(GCHandle) && other != null &&
            ((Delegate)_handle.Target).Method.Equals(other.Method);
    }
    /// <summary>
    /// 释放弱引用
    /// </summary>
    ~WeakReference() {
        _handle.Free();
    }
}

我实现了IEquatable<Delegate>接口,该接口能方便的比较WeakReference实例和委托是否指一个方法。利用该类,就可以写一个自己的弱事件封装器。


public class WeakEventManager {
    private readonly List<WeakReference<Delegate>> _delegateList;
    public WeakEventManager() {
        _delegateList = new List<WeakReference<Delegate>>();
    }

    /// <summary>
    /// 订阅
    /// </summary>
    public void AddHandler(Delegate handler) {
        if (handler != null)
            _delegateList.Add(new WeakReference<Delegate>(handler));
    }

    /// <summary>
    /// 取消订阅
    /// </summary>
    public void RemoveHandler(Delegate handler) {
        if (handler == null) return;
        //由于我实现了IEquatable<Delegate>,这里能够很方便的比较
        var sameHandler = _delegateList.FirstOrDefault(e => e.Equals(handler));
        if (sameHandler != null)
            _delegateList.Remove(sameHandler);
    }

    /// <summary>
    /// 引发事件
    /// </summary>
    public void Raise(object sender, EventArgs e) {
        foreach (var d in _delegateList.ToList()) {
            if (d.IsAlive)
                d.Target.DynamicInvoke(sender, e);
            else
                _delegateList.Remove(d);
        }
    }
}

最后,就可以像下面这样定义自己的事件了


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



作者:kalanliao
链接:https://www.jianshu.com/p/aaa2de7782f0
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

C#内存泄漏 非托管堆内存泄漏 <br />刚进实验室,老师给我一项任务:一个已经开发好的软件,有两个BUG,要我去改。软件是用C#写的,这个以前没搞过C#。所以就一边看书学习一边看代码。<br />第一个BUG其实严格来说不叫BUG,而是功能的完善:目前软件的日志只能在本软件中保存和打开查看,我需要把日志以Word文档形式导出来。这个功能的实现我用了大概一周就基本完成了。后来就把精力全放在第二个BUG上了。第二个BUG是:本软件要调用外部COM组件,但是在执行某些COM组件时,程序运行到一半就自行退出,崩溃了。听说以前有多个同学搞过这个 阅读详情

相关推荐

Godot 3.5 C#开发避坑指南:信号连接、场景切换与随机生成实战

在游戏开发中,事件驱动架构通过信号与委托机制实现模块解耦,其核心原理是观察者模式,允许对象间进行松耦合通信。这一机制的技术价值在于提升代码的可维护性和可扩展性。在Godot引擎中,信号系统被广泛应用于UI交互、游戏状态管理和对象生命周期事件处理。然而,当开发者从GDScript转向C#时,会遇到信号连接因委托与Callable混淆而失效,以及因未及时断开连接导致的内存泄漏问题。本文聚焦于Godot 3.5的C#开发,针对这些高频痛点,结合场景切换时的资源清理与随机数生成器的正确使用,提供了避免内存泄漏和确保

weixin_29007809的博客 278

C#event内存泄漏总结

本篇文章给大家分享了在C#event内存泄漏的相关知识点以及代码分享,有兴趣的朋友参考学习下。

Unity开发中C#委托内存泄漏的成因、排查与最佳实践

C#编程中,委托(Delegate)作为实现事件驱动和回调机制的核心技术,其本质是一个封装了方法引用的类型。它通过维护一个调用列表,允许将多个方法动态关联与执行,是实现观察者模式和解耦组件通信的关键。然而,在拥有独特生命周期管理的Unity引擎中,委托的引用机制与GameObject的销毁逻辑若未妥善协调,极易引发隐蔽的内存泄漏问题,导致托管堆内存持续增长,影响应用性能与稳定性。理解委托在Unity中的内存陷阱,掌握其与对象生命周期、匿名方法捕获等特性的交互原理,是进行性能优化和构建健壮游戏项目的必备技能

weixin_30709061的博客 440

C#内存泄漏原因整理

C#内存泄漏原因整理

雪中冰雨 4612

C# 处理内存泄漏问题

C# 处理内存溢出问题

chaoge_dgqb的博客 3598

C#委托与回调实战避坑指南:从内存泄漏到异步陷阱

C#编程中,委托作为一种类型安全的方法引用机制,是实现回调设计模式的核心技术手段。其原理在于通过定义方法签名契约,允许将方法作为参数传递或存储,从而实现松耦合的组件通信。这一机制的技术价值在于提升代码的灵活性和可扩展性,广泛应用于事件驱动编程、异步操作通知和插件系统等场景。然而,在实际工程实践中,开发者常因忽略委托链的异常传播特性或未妥善管理事件订阅生命周期而引发内存泄漏问题。特别是在结合async/await进行异步编程时,执行上下文的丢失可能导致UI更新异常。本文聚焦于C#委托与回调的典型使用误区,通

weixin_31714129的博客 209

Unity事件分发系统全解析:从UnityEvent到全局事件中心的架构实践

在软件架构中,观察者模式是实现模块间松耦合通信的核心设计模式,它通过发布-订阅机制让组件无需直接引用即可交互。其原理是定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会自动收到通知。这一模式的技术价值在于大幅提升了代码的可维护性、可测试性和可扩展性,是构建复杂交互系统的基石。在游戏开发领域,尤其是在Unity引擎中,事件分发系统正是该模式的典型实现,广泛应用于UI交互响应、游戏逻辑解耦、状态同步等场景。本文聚焦于Unity事件系统的工程实践,深入剖析UnityEvent的可视化绑定机制与

weixin_28049429的博客 267

C#内存泄漏--event内存泄漏

内存泄漏是指:当一块内存被分配后,被丢弃,没有任何实例指针指向这块内存, 并且这块内存不会被GC视为垃圾进行回收。这块内存会一直存在,直到程序退出。C#是托管型代码,其内存的分配和释放都是由CLR负责,当一块内存没有任何实例引用时,GC会负责将其回收。既然没有任何实例引用的内存会被GC回收,那么内存泄漏是如何发生的? 内存泄漏示例   为了演示内存泄漏是如何发生的,我们来看一段...

weixin_30657541的博客 738

C# 事件(Event)用法

任何现有的事件订阅者都是遵循经典模式的事件的订阅者。第二个参数:e是EventArgs类型 EventArgs类表示包含事件数据的类的基类,并提供用于不包含事件数据的事件的值。一个对象要想被作为事件监听器,需要将该对象中的事件处理程序 注册(或挂载)到另一个能够产生事件的对象(即事件源)上,这样会导致事件源必须保持一个到事件侦听器对象的引用,以便在事件发生时调用此侦听器的处理方法。这很合理,但如果这个引用是一个 强引用,则监听器会作为事件源的一个依赖 从而不能作为垃圾回收,即使引用它的对象是事件源。...

weixin_41883890的博客 4256

搞懂事件——C#event的机制深度理解

为什么忽然对Event感兴趣了?因为进入Web时代以后,很少使用它了,忽然想起这个知识点,感到非常的熟悉而陌生。不知道你有没有类似的感觉:好像对某个点大脑很熟悉,而手又很陌生?就像多年未曾谋面的老朋友,一个瞬间涌入大脑很多往日嬉戏打闹的碎片,但念及当先,却又音信全无。那么,你有没有事件相关的疑惑呢?Event 是同步还是异步执行的?如果是多个订阅,事件执行的顺序是什么?如果事件执行中发生异常,会发生什么事情?事件支持异步执行吗?事件触发后,跨进程可以触发到吗?

网尘的技术专栏 1027

使用 Diff 功能洞察 C# 内存泄漏增量

其实非常好理解,就是当你的程序出现了内存泄漏,你可以在程序内存增长的过程中截取两个 dump 文件,然后通过 PerfView 观察其中的内存增量是什么?帮助我们快速找出可能被泄漏的对象。当然你用 WinDBG 的话也是没有问题的,只不过需要用肉眼扫一下而已,接下来举两个例子说明一下。

shengyin714959的博客 100

PerfView专题 (第十一篇):使用 Diff 功能洞察 C# 内存泄漏增量

其实非常好理解,就是当你的程序出现了内存泄漏,你可以在程序内存增长的过程中截取两个 dump 文件,然后通过 PerfView 观察其中的内存增量是什么?帮助我们快速找出可能被泄漏的对象。当然你用 WinDBG 的话也是没有问题的,只不过需要用肉眼扫一下而已,接下来举两个例子说明一下。...

一线码农的专栏 1032

32.C# 事件(Event)——《跟老吕学C#

C#中,事件是通过event关键字声明的。事件通常基于委托类型,委托定义了事件的签名,即事件的参数和返回类型。事件的声明通常放在类的内部,作为类的一个成员。// 声明事件,基于MyEventHandler委托类型除了使用内置的委托类型(如等)外,我们还可以根据需要定义自定义的委托类型和事件。这允许我们更灵活地控制事件的参数和返回类型。我们还可以定义自定义的事件访问器来控制对事件的访问权限。例如,可以定义只读的私有事件来限制外部对象对事件的访问。

Python老吕的博客 1165

C#委托与事件混淆导致内存泄漏?一文看懂底层机制与避坑指南

掌握C#委托与事件区别,避免内存泄漏难题。深入解析委托与事件的底层机制,对比适用场景与核心用法,揭示事件封装带来的安全优势。通过典型示例展示正确使用方式,帮助开发者规避常见陷阱,提升代码稳定性,值得收藏。

CodeVibe的博客 1045

Unity C# 委托与事件 3 大核心差异:从访问权限到内存泄漏实战解析

本文深入解析Unity C#中委托与事件的三大核心差异,包括访问权限、内存泄漏风险及设计模式应用。通过实战案例展示如何避免常见陷阱,如封装性差异导致的安全问题和事件订阅引发的内存泄漏,帮助开发者编写更安全高效的代码。

weixin_34306593的博客 416
上一篇: 针孔相机拍摄的图像坐标和空间点的对应关系
下一篇: 尊重工程师的时代终将到来
nodeathphoenix
博客等级 码龄16年 97粉丝 99原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值