为什么定时器让我的对象保持活着?

前言 :我知道如何解决问题。 我想知道它为什么会出现。 请从上到下阅读问题。

正如我们都应该知道的那样,添加事件处理程序会导致C#中的内存泄漏。 请参阅为什么以及如何避免事件处理程序内存泄漏?

另一方面,对象通常具有相似或相关的生命周期,并且不需要取消注册事件处理程序。 考虑这个例子:

using System; public class A { private readonly B b; public A(B b) { this.b = b; b.BEvent += b_BEvent; } private void b_BEvent(object sender, EventArgs e) { // NoOp } public event EventHandler AEvent; } public class B { private readonly A a; public B() { a = new A(this); a.AEvent += a_AEvent; } private void a_AEvent(object sender, EventArgs e) { // NoOp } public event EventHandler BEvent; } internal class Program { private static void Main(string[] args) { B b = new B(); WeakReference weakReference = new WeakReference(b); b = null; GC.Collect(); GC.WaitForPendingFinalizers(); bool stillAlive = weakReference.IsAlive; // == false } } 

AB通过事件隐式地相互引用,但GC可以删除它们(因为它不使用引用计数,而是标记和扫描)。

但现在考虑这个类似的例子:

 using System; using System.Timers; public class C { private readonly Timer timer; public C() { timer = new Timer(1000); timer.Elapsed += timer_Elapsed; timer.Start(); // (*) } private void timer_Elapsed(object sender, ElapsedEventArgs e) { // NoOp } } internal class Program { private static void Main(string[] args) { C c = new C(); WeakReference weakReference = new WeakReference(c); c = null; GC.Collect(); GC.WaitForPendingFinalizers(); bool stillAlive = weakReference.IsAlive; // == true ! } } 

为什么GC不能删除C对象? 为什么Timer保持对象存活? 定时器机制的一些“隐藏”参考(例如静态参考)是否使计时器保持活动状态?

(*)注意:如果仅创建但未启动计时器,则不会发生此问题。 如果它已启动并稍后停止,但事件处理程序未取消注册,则问题仍然存在。

定时器逻辑依赖于OSfunction。 它实际上是触发事件的操作系统。 OS依次使用CPU中断来实现。

OS API,即Win32,不包含对任何类型的任何对象的引用。 它保存了在发生计时器事件时必须调用的函数的内存地址。 .NET GC无法跟踪此类“引用”。 因此,可以收集计时器对象而无需取消订阅低级事件。 这是一个问题,因为OS无论如何都会尝试调用它,并会因一些奇怪的内存访问exception而崩溃。 这就是.NET Framework在静态引用的对象中保存所有此类计时器对象并仅在取消订阅时将其从该集合中删除的原因。

如果您使用SOS.dll查看对象的根目录,您将获得下一张图片:

 !GCRoot 022d23fc HandleTable: 001813fc (pinned handle) -> 032d1010 System.Object[] -> 022d2528 System.Threading.TimerQueue -> 022d249c System.Threading.TimerQueueTimer -> 022d2440 System.Threading.TimerCallback -> 022d2408 System.Timers.Timer -> 022d2460 System.Timers.ElapsedEventHandler -> 022d23fc TimerTest.C 

然后,如果你看看像dotPeek这样的System.Threading.TimerQueue类,你会看到它被实现为一个单例并且它包含一组计时器。

这是它的工作原理。 不幸的是,MSDN文档并不是很清楚。 他们只是假设如果它实现了IDisposable,那么你应该处理它,不要问。

定时器机制的一些“隐藏”参考(例如静态参考)是否使计时器保持活动状态?

是。 它是在CLR中构建的,当您使用引用源或反编译器(Timer类中的私有“cookie”字段)时,您可以看到它的痕迹。 它作为第二个参数传递给System.Threading.Timer构造函数,该构造函数实际实现了计时器,即“状态”对象。

CLR保留已启用的系统计时器列表,并添加对状态对象的引用,以确保它不会被垃圾回收。 这反过来确保Timer对象不会被垃圾收集,只要它在列表中。

因此,收集System.Timers.Timer垃圾需要您调用其Stop()方法或将其Enabled属性设置为false,同样的事情。 这导致CLR从活动计时器列表中删除系统计时器。 这也删除了对state对象的引用。 然后,这使得计时器对象有资格进行收集。

显然这是理想的行为,你通常不希望计时器消失并在它处于活动状态时停止滴答。 当您使用System.Threading.Timer时发生这种情况,如果您没有显式地或使用状态对象保留对它的引用,它将停止调用它的回调。

我认为这与Timer的实现方式有关。 当你调用Timer.Start()时,它设置Timer.Enabled = true。 看看Timer.Enabled的实现:

 public bool Enabled { [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] get { return this.enabled; } set { if (base.DesignMode) { this.delayedEnable = value; this.enabled = value; } else if (this.initializing) { this.delayedEnable = value; } else if (this.enabled != value) { if (!value) { if (this.timer != null) { this.cookie = null; this.timer.Dispose(); this.timer = null; } this.enabled = value; } else { this.enabled = value; if (this.timer == null) { if (this.disposed) { throw new ObjectDisposedException(base.GetType().Name); } int dueTime = (int) Math.Ceiling(this.interval); this.cookie = new object(); this.timer = new Timer(this.callback, this.cookie, dueTime, this.autoReset ? dueTime : 0xffffffff); } else { this.UpdateTimer(); } } } } } 

它看起来像是一个新的计时器,传递给它一个cookie对象(很奇怪!)。 在该调用路径之后会导致一些其他复杂的代码,包括创建TimerHolder和TimerQueueTimer。 我希望在某些时候创建一个在Timer外部保持的引用,直到你调用Timer.Stop()或Timer.Enabled = false为止。

这不是一个明确的答案,因为我发布的代码都没有创建这样的引用; 但它的内心很复杂,导致我怀疑这样的事情正在发生。

如果你有reflection器(或类似)看看,你会明白我的意思。 🙂

因为Timer仍处于活动状态。 ( Timer.Elapsed不会删除事件处理程序)。

如果要正确处置,请实现IDisposable接口,在Dispose方法中删除事件处理程序,并使用using块或手动调用Dispose 。 问题不会发生。

  public class C : IDisposable { ... void Dispose() { timer.Elapsed -= timer_elapsed; } } 

然后

  C c = new C(); WeakReference weakReference = new WeakReference(c); c.Dispose(); c = null; 

我认为问题出在这条线上;

 c = null; 

通常,大多数开发人员认为使对象等于null会导致对象被垃圾回收器删除。 但这种情况并非如此; 实际上只删除了对内存位置(创建c对象)的引用; 如果对相关内存位置有任何其他引用,则不会将对象标记为删除。 在这种情况下,由于Timer正在引用相关的内存位置,因此垃圾收集器不会删除对象。

我们先来谈谈Threading.Timer。 在内部,计时器将使用回调和状态传递给Timer ctor来构造TimerQueueTimer对象(比如新的Threading.Timer(回调,状态,xxx,xxx).TimerQueueTimer将被添加到静态列表中。

如果回调方法和状态没有“this”信息(比如使用静态方法进行回调而null为状态),那么Timer对象可以在没有引用时进行GCed。 另一方面,如果成员方法用于回调,则包含“this”的委托将存储在上述静态列表中。 所以Timer对象不能被GC,因为仍然引用了“C”(在你的例子中)对象。

现在让我们回到System.Timers.Timer,它内部包装了Threading.Timer。 请注意,当前者构造后者时,将使用System.Timers.Timer成员方法,因此无法对System.Timers.Timer对象进行GCed。