【问题标题】:What kind of memory leaks do lambda event handlers cause?lambda 事件处理程序会导致什么样的内存泄漏?
【发布时间】:2019-01-17 10:31:41
【问题描述】:

事件处理程序很容易导致内存泄漏,因为事件的调用列表持有对事件处理实例的引用,因此如果事件源还活着,事件处理实例就不能被垃圾回收。

但请考虑以下代码:

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 事件处理程序中。我改写了问题/标题以使这一点更清楚。

【问题讨论】:

标签: c# events memory-leaks


【解决方案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 版: 我提供的 2 个链接给一个 kick lambda 将被编译为 MyClass 方法,该方法将被 SomeClass 实例捕获,如 版本 1

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

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

你也有'object x',所以编译器生成的类将被创建,它 将包含公共字段 public object x; 和将从您的 lambda 转换的方法(请参阅实施示例段落)。

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

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

在SomeClass事件在调用列表中有它的方法之前,GC不会收集任何类的任何实例。

【讨论】:

  • 感谢您的精心回复。我猜关键语句是 “在 SomeClass 事件在调用列表中有它的方法之前,GC 不会收集任何类的任何实例。”。而且,多亏您的解释,泄露了哪个实例也很清楚。
  • @mike 不客气。是的,这是关键声明,您也可以在问题下方第一条评论中的链接提供的答案中阅读它。
猜你喜欢
  • 1970-01-01
  • 2012-11-02
  • 1970-01-01
  • 2010-09-06
  • 2023-03-11
  • 2011-11-14
  • 2011-01-13
  • 2012-04-20
  • 2011-06-25
相关资源
最近更新 更多