我所知道的在C#中引起非意识性内存泄漏的两种方法:
- 未处置实现了
IDisposable
的资源。 - 错误地引用和取消引用事件。
我不太理解第二点。如果源对象的生命周期比监听器长,当没有其他引用时监听器不再需要事件,使用普通的.NET事件会导致内存泄漏:源对象会将监听器对象持有在内存中,而这些对象应该被垃圾回收。
请问如何通过C#代码解释事件如何导致内存泄漏,并编写代码以避免使用弱引用和不使用弱引用?
我所知道的在C#中引起非意识性内存泄漏的两种方法:
IDisposable
的资源。我不太理解第二点。如果源对象的生命周期比监听器长,当没有其他引用时监听器不再需要事件,使用普通的.NET事件会导致内存泄漏:源对象会将监听器对象持有在内存中,而这些对象应该被垃圾回收。
请问如何通过C#代码解释事件如何导致内存泄漏,并编写代码以避免使用弱引用和不使用弱引用?
当监听器将事件监听器附加到事件时,源对象将获得对监听器对象的引用。这意味着,直到事件处理程序被分离或源对象被收集之前,监听器都不能被垃圾收集器收回。
考虑以下类:
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
}
}
...然后是以下代码:
Source newSource = new Source();
Listener listener = new Listener(newSource);
listener = null;
即使我们将null
分配给listener
,由于newSource
仍然持有对事件处理程序(Listener.source_SomeEvent
)的引用,因此它不会符合垃圾回收条件。为了解决这种泄漏问题,在不再需要它们时,始终分离事件侦听器非常重要。
上述示例是为了集中展示泄漏问题而编写的。为了修复该代码,最简单的方法可能是让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;
}
}
}
然后调用代码可以使用该对象发出完成信号,这将删除Source
对`Listener`的引用;
Source newSource = new Source();
Listener listener = new Listener(newSource);
// use listener
listener.Close();
listener = null;
Listener
没有持有对 Source
的引用。Listener
之所以能够保持存活是因为 Source
持有对 Listener
的引用。只要有任何东西持有对 Source
的引用,Listener
就无法被回收,但是 Listener
并不会阻止 Source
被回收。 - Fredrik Mörknull
分配给监听器实际上并没有做任何事情,除了告诉垃圾收集器该项已准备好被收集;一旦它离开块,垃圾收集器就会这样做。很少需要将对象设置为null以使其在离开块后被收集,但有些程序员将其放在那里以明确说明他们将对象设置为null。 - George Stockerprivate Source _source;
中,对吧?@GeorgeStocker?还是他在谈论第一段代码? - Royi Namir阅读Jon Skeet关于事件的优秀文章。 这不是经典意义上的“内存泄漏”,而更像是未被断开的保留引用。 因此,请始终记住,要使用-=
取消之前使用+=
添加的事件处理程序,您就应该没问题了。
IDisposable
,并应在 Dispose
中取消订阅其事件,但 Microsoft 并不容易始终如一地做到这一点,不幸的是粗心大意通常不会引起麻烦。 - supercatMulticastDelegate
本身不会对 IDisposable
进行任何操作。调用 Delegate.Remove
(通常是取消事件订阅的方法)只会创建一个不包含已删除项的新的 MulticastDelegate
。针对事件,终结器通常是无用的,因为只要发布者在作用域内,事件将保持其所有订阅者的作用域。到订阅者可能被终结时,发布者将超出作用域并且事件将变得无关紧要。 - supercat
IDisposable
并不会导致内存泄漏。IDisposable
存在的原因是为了实现“资源的确定性释放”。换句话说,开发人员可以精确控制资源何时被释放。总的来说,即使是IDisposable
对象,最终也会被垃圾回收器处理,但你无法确定具体的时间。 - Jesse C. Slicer