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

Seb*_*zus 7 c# events timer

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

正如我们都应该知道的那样,添加事件处理程序会导致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
    }
}
Run Code Online (Sandbox Code Playgroud)

A并且B通过事件隐式地引用彼此,但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 !
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

Den*_*nis 5

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

OS API,即Win32,不包含对任何类型的任何对象的引用.它保存了在发生计时器事件时必须调用的函数的内存地址..NET GC无法跟踪此类"引用".因此,可以收集计时器对象而无需取消订阅低级事件.这是一个问题,因为OS无论如何都会尝试调用它,并且会因一些奇怪的内存访问异常而崩溃.这就是.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
Run Code Online (Sandbox Code Playgroud)

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

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