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

4

事件处理程序很容易导致内存泄漏,因为事件的调用列表保存了对事件处理实例的引用,所以如果事件源仍然存在,则无法回收事件处理实例。

但请考虑以下代码:

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内部的代码没有引用MyClass的实例?
  • 版本3:因为lambda包含一个闭包,它本身是对MyClass实例的引用——或者不是吗?
  • 版本4:因为lambda没有引用MyClass实例,所以这可能不会导致泄漏?

关于版本3和4的后续问题:

  • "魔术帮助对象"在哪里存储,.Net为lambda/闭包创建它,并且它是否(总是)包含一个引用,可以使MyClass实例保持活动状态?
  • 如果lambda事件处理程序可能会泄漏,那么它们只能在不会出现问题的情况下使用(例如,MyClass实例比SomeClass实例更长寿),因为它们不能使用-=进行删除?

编辑:此帖子(原标题为“何时会导致事件处理程序内存泄漏?”)被建议作为重复项,链接为Why and How to avoid Event Handler memory leaks?,但我不同意,因为该问题是针对lambda事件处理程序的。我重新表述了问题/标题,以使这一点更加清晰。


可能是为什么以及如何避免事件处理程序内存泄漏?的重复问题。 - Klaus Gütter
我不认为这是一个重复的问题,因为我的问题超出了您链接中的问题,特别是关于将lambda用作事件处理程序的部分。 - mike
1个回答

1

免责声明: 我不能保证这是100%的真相 - 您的问题非常深入,我可能会犯错。

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

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

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

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

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

除非局部变量以某种方式被捕获,否则也与编译器工作有关,您可以在Jon Skeet的网站上阅读相关内容:

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

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

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

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

版本 4: 我提供的两个链接中,kick lambda 将会被编译成 MyClass 方法,该方法将被 SomeClass 实例捕获,就像版本 1 中一样。

版本 2: 我不知道本地方法将如何编译,但应该与版本 4(因此也是版本 1)相同。

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

您还有 'object x',因此编译器将生成一个类,其中包含公共字段public object x;和从 lambda 翻译而来的方法(请参见 实现示例 段落)。

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

版本 3中,编译器将生成一个类,并且它将保存您的本地变量和从 lamdba 翻译而来的方法。

任何类的任何实例在其方法被添加到调用列表中之前都不会被GC回收。

感谢您详细的回复。我想关键的陈述是“除非SomeClass事件在调用列表中具有其方法,否则任何类的任何实例都不会被GC收集。”而且,由于您的解释,现在也清楚哪个实例泄漏了。 - mike
@mike 不客气。是的,这是关键语句,你也可以在你问题下面第一个评论提供的链接中找到它。 - Woldemar89

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接