简体   繁体   English

事件如何导致C#中的内存泄漏?弱引用如何帮助缓解这种情况?

[英]How do events cause memory leaks in C# and how do Weak References help mitigate that?

There are two ways (that I know of) to cause an unintentional memory leak in C#: 有两种方法(我知道)在C#中导致无意的内存泄漏:

  1. Not disposing of resources that implement IDisposable 不处理实现IDisposable的资源
  2. Referencing and de-referencing events incorrectly. 不正确地引用和取消引用事件。

I don't really understand the second point. 我真的不明白第二点。 If the source object has a longer lifetime than the listener, and the listener doesn't need the events anymore when there are no other references to it, using normal .NET events causes a memory leak: the source object holds listener objects in memory that should be garbage collected. 如果源对象的生命周期比侦听器长,并且当没有其他引用时,侦听器不再需要事件,则使用普通的.NET事件会导致内存泄漏:源对象将侦听器对象保存在内存中应该是垃圾收集。

Can you explain how events can cause memory leaks with code in C#, and how I can code to get around it using Weak References and without Weak References? 你能用C#中的代码解释事件如何导致内存泄漏,以及如何使用弱引用和没有弱引用来编写代码来解决它?

When a listener attaches an event listener to an event, the source object will get a reference to the listener object. 当侦听器将事件侦听器附加到事件时,源对象将获得对侦听器对象的引用。 This means that the listener cannot be collected by the garbage collector until either the event handler is detached, or the source object is collected. 这意味着在分离事件处理程序或收集源对象之前,垃圾收集器无法收集侦听器。

Consider the following classes: 考虑以下类:

class Source
{
    public event EventHandler SomeEvent;
}

class Listener
{
    public Listener(Source source)
    {
        // attach an event listner; this adds a reference to the
        // source_SomeEvent method in this instance to the invocation list
        // of SomeEvent in source
        source.SomeEvent += new EventHandler(source_SomeEvent);
    }

    void source_SomeEvent(object sender, EventArgs e)
    {
        // whatever
    }
}

...and then the following code: ...然后是以下代码:

Source newSource = new Source();
Listener listener = new Listener(newSource);
listener = null;

Even though we assign null to listener , it will not be eligible for garbage collection, since newSource is still holding a reference to the event handler ( Listener.source_SomeEvent ). 即使我们为listener分配null ,它也不符合垃圾收集的条件,因为newSource仍然保持对事件处理程序的引用( Listener.source_SomeEvent )。 To fix this kind of leak, it is important to always detach event listeners when they are no longer needed. 要解决此类泄漏,在不再需要事件侦听器时始终分离事件侦听器非常重要。

The above sample is written to focus on the problem with the leak. 编写上述示例以关注泄漏问题。 In order to fix that code, the easiest will perhaps be to let Listener hold on to a reference to Source , so that it can later detach the event listener: 为了修复该代码,最简单的方法可能是让Listener保持对Source的引用,以便以后可以分离事件监听器:

class Listener
{
    private Source _source;
    public Listener(Source source)
    {
        _source = source;
        // attach an event listner; this adds a reference to the
        // source_SomeEvent method in this instance to the invocation list
        // of SomeEvent in source
        _source.SomeEvent += source_SomeEvent;
    }

    void source_SomeEvent(object sender, EventArgs e)
    {
        // whatever
    }

    public void Close()
    {
        if (_source != null)
        {
            // detach event handler
            _source.SomeEvent -= source_SomeEvent;
            _source = null;
        }
    }
}

Then the calling code can signal that it is done using the object, which will remove the reference that Source has to ´Listener`; 然后调用代码可以发信号通知它是使用对象完成的,这将删除Source对'Listener`的引用;

Source newSource = new Source();
Listener listener = new Listener(newSource);
// use listener
listener.Close();
listener = null;

Read Jon Skeet's excellent article on events. 阅读Jon Skeet关于事件的精彩文章 It's not a true "memory leak" in the classic sense, but more of a held reference that hasn't been disconnected. 这不是传统意义上的真正的“内存泄漏”,而是更多的未被断开的保持引用。 So always remember to -= an event handler that you += at a previous point and you should be golden. 所以要记住-=一个事件处理程序,你在前一点+=并且你应该是金色的。

There are, strictly speaking, no "memory leaks" within the "sandbox" of a managed .NET project; 严格来说,在托管的.NET项目的“沙箱”中没有“内存泄漏”; there are only references held longer than the developer would think necessary. 只有开发人员认为必要的参考时间才会更长。 Fredrik has the right of it; 弗雷德里克有权利; when you attach a handler to an event, because the handler is usually an instance method (requiring the instance), the instance of the class containing the listener stays in memory as long as this reference is maintained. 当您将处理程序附加到事件时,因为处理程序通常是实例方法(需要实例),所以只要维护此引用,包含侦听器的类的实例就会保留在内存中。 If the listener instance contains references to other classes in turn (eg backreferences to containing objects), the heap can stay quite large long after the listener has gone out of all other scopes. 如果侦听器实例依次包含对其他类的引用(例如,对包含对象的反向引用),则在侦听器退出所有其他范围之后,堆可以保持很长时间。

Maybe someone with a bit more esoteric knowledge of Delegate and MulticastDelegate can shed some light on this. 也许对Delegate和MulticastDelegate有更多深奥知识的人可以对此有所了解。 The way I see it, a true leak COULD be possible if all of the following were true: 我看到它的方式,如果满足以下所有条件,则可能出现真正的泄漏:

  • The event listener requires external/unmanaged resources to be released by implementing IDisposable, but it either does not, or 事件侦听器需要通过实现IDisposable来释放外部/非托管资源,但它要么没有,要么
  • The event multicast delegate does NOT call the Dispose() methods from its overridden Finalize() method, and 事件多播委托不会从其重写的Finalize()方法调用Dispose()方法,并且
  • The class containing the event does not call Dispose() on each Target of the delegate through its own IDisposable implementation, or in Finalize(). 包含事件的类不会通过自己的IDisposable实现或Finalize()在委托的每个Target上调用Dispose()。

I've never heard of any best practice involving calling Dispose() on delegate Targets, much less event listeners, so I can only assume the .NET developers knew what they were doing in this case. 我从来没有听说过任何涉及在委托目标上调用Dispose()的最佳实践,更不用说事件监听器了,所以我只能假设.NET开发人员知道他们在这种情况下做了什么。 If this is true, and the MulticastDelegate behind an event tries to properly dispose of listeners, then all that is necessary is proper implementation of IDisposable on a listening class that requires disposal. 如果这是真的,并且事件后面的MulticastDelegate尝试正确处理侦听器,那么所有必要的是在需要处理的侦听类上正确实现IDisposable。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM