lambda事件处理程序会导致哪种内存泄漏?

时间:2019-01-17 10:31:41

标签: c# events memory-leaks

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

但是请考虑以下代码:

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的引用暗示了对MyClass实例的引用-除非,如果localHandler中的代码没有对{{ 1}}?
  • 版本3:因为lambda包含一个闭包,而闭包本身就是对MyClass实例的引用-是吗?
  • 版本4:因为lambda没有引用MyClass的实例,这可能不会导致泄漏吗?

然后,跟踪版本3和4的问题:

  • .Net为lambda / closure创建的“魔术助手对象”存储在哪里,并且它(总是)包含使MyClass实例保持活动状态的引用吗?
  • 如果lambda事件处理程序可能泄漏,则应仅在没有问题的情况下使用它们(例如MyClass实例在MyClass实例之后),因为无法使用{{1}删除它们}?

编辑:这篇文章(原标题为“何时事件处理程序会导致内存泄漏?”)被建议与Why and How to avoid Event Handler memory leaks?重复,但我不同意,因为这个问题专门针对lambda事件处理程序。我改掉了这个问题/标题,以使其更加清楚。

1 个答案:

答案 0 :(得分:1)

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

但是,我希望它能给您一些想法或方向。

让我们根据CLR记忆组织来考虑这个问题:

局部方法变量和方法参数存储在内存中的方法堆栈框架中(除非它们用ref关键字声明)。

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

方法执行时存在方法堆栈框架,方法结束后局部方法变量将随堆栈框架消失。

除了以一种或另一种方式捕获局部变量外,它还与编译器工作有关,您可以在Jon Skeet的网站上了解它:

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

版本1 OnSomeEvent方法是MyClass的成员,它将被Someclass source实例捕获,直到引用该方法的委托不会被删除。从事件。因此,MyClass不会收集在构造函数中创建并放置在堆中并保存此方法的GC实例,直到不会从事件中删除其方法引用为止。

编译器通过特定方式编译lambda,请完整阅读实施示例段落:

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

版本4 : 我提供的提供踢Lambda的2个链接将被编译为MyClass方法,该方法将由SomeClass实例捕获,如 Version 1

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

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

您也有'object x',因此将创建编译器生成的类,该类 将包含公共字段public object x;和将从lambda转换而来的方法(请参见实施示例段落)。

因此,我认为在 1、2、4版在内部是相同的: MyClass将包含将用作事件处理程序的方法。

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

直到SomeClass事件在调用列表中有其方法,GC才会收集任何类的任何实例。