代码之家  ›  专栏  ›  技术社区  ›  mike

lambda事件处理程序导致什么类型的内存泄漏?

  •  1
  • mike  · 技术社区  · 7 年前

    事件处理程序很容易导致内存泄漏,因为事件的调用列表包含对事件处理实例的引用,因此如果事件源仍然活动,则无法对事件处理实例进行垃圾收集。

    但请考虑以下代码:

    public class SomeClass
    {
        public event EventHandler SomeEvent;
    }
    public class MyClass
    {
        public MyClass(SomeClass source)
        {
            //VERSION 1
            source.SomeEvent += OnSomeEvent;
    
            //VERSION 2
            void localHandler(object s, EventArgs args) { Console.WriteLine("some action with(out) any references"); }
            source.SomeEvent += localHandler;
    
            //VERSION 3
            var x = new object();
            source.SomeEvent += (s, e) => { Console.WriteLine("some event fired, using closure x:" + x.GetHashCode()); };
    
            //VERSION 4
            source.SomeEvent += (s, e) => { Console.WriteLine("some action without any references"); };
        }
    
        private void OnSomeEvent(object sender, EventArgs e) 
        {
            //...
        }
    }
    

    我的假设/问题:为什么不同的事件处理版本可能导致内存泄漏:

    • 版本1:因为调用目标明确引用了 MyClass .
    • 版本2:因为引用 localHandler 表示引用的实例 类名 -除非,也许,如果里面的代码 本地处理器 没有对的实例的引用 类名 ?
    • 版本3:因为lambda包含一个闭包,它本身是对 类名 或者是吗?
    • 版本4:因为lambda没有引用的实例 类名 ,这可能不会导致泄漏?

    以及版本3和版本4的后续问题:

    • 其中是.NET为存储的lambda/闭包创建的“magic helper对象”,它(总是)是否包含将保留 类名 活着?
    • 如果lambda事件处理程序可能泄漏,则只应在没有问题的情况下使用它们(例如 类名 实例异常 SomeClass 实例),因为不能使用 -= ?

    编辑: 这篇文章(原始标题为“事件处理程序何时会导致内存泄漏?”)建议作为副本,共 Why and How to avoid Event Handler memory leaks? 但我不同意,因为这个问题专门针对lambda事件处理程序。我重新表述了问题/标题,以便更清楚地说明这一点。

    1 回复  |  直到 7 年前
        1
  •  1
  •   Vladimir P.    7 年前

    免责声明 : 我不能保证这是100%的事实-你的问题很深,我可能会犯错误。

    不过,我希望它能给你一些想法或方向。

    让我们考虑这个问题 CLR 内存组织:

    局部方法变量和方法参数存储在内存中的方法堆栈帧中(用 ref 关键字)。

    堆栈存储值类型和引用类型变量引用,这些引用指向堆中的对象。

    方法执行时存在方法堆栈帧,方法结束后,局部方法变量将与堆栈帧一起显示。

    除了以某种方式捕获局部变量外,它还与 编译程序 工作,你可以在乔恩·斯基特的网站上阅读:

    http://jonskeet.uk/csharp/csharp2/delegates.html#captured.variables

    版本1 : OnSomeEvent 方法是的成员 MyClass 它将被 Someclass source 实例,直到引用此方法的委托不会从事件中移除。所以, 类名 在构造函数中创建、放置在堆中并保留此方法的实例将不会由 GC 直到其方法引用不会从事件中移除。

    编译程序 按特定方式编译lambda,请阅读 实施例 完全段落:

    https://github.com/dotnet/csharplang/blob/master/spec/conversions.md#anonymous-function-conversions

    版本4 : 我提供的两个链接将编译为 类名 方法,将由 SomeClass 实例如 版本1

    版本2 : 我不知道如何编译本地方法的细微差别,但应该与中的相同 版本4 (因此, 版本1 )

    版本3 : 所有的局部变量都将被有趣的方式捕获。

    您还有“object x”,因此将创建编译器生成的类,该类 将包含公共字段 public object x; 以及将从lambda转换的方法(请参见 实施例 段落)。

    所以,我认为 1,2,4版本 内部相同: MyClass将包含将用作事件处理程序的方法。

    在 版本3 将创建编译器生成的类,它将保存从lamdba转换的本地变量和方法。

    任何类的任何实例都不会由GC收集,直到 身体类 事件的方法在调用列表中。