7

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

我们都(应该)知道,添加事件处理程序会导致 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 使对象保持活动状态?计时器是否通过计时器机制的一些“隐藏”参考(例如静态参考)保持活动状态?

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

4

6 回答 6

5

定时器逻辑依赖于操作系统功能。实际上是操作系统触发了事件。操作系统反过来使用CPU 中断来实现它。

OS API,又名 Win32,不包含对任何类型的任何对象的引用。它保存在定时器事件发生时必须调用的函数的内存地址。.NET GC 无法跟踪此类“引用”。结果,可以在不取消订阅低级别事件的情况下收集计时器对象。这是一个问题,因为操作系统无论如何都会尝试调用它,并且会因一些奇怪的内存访问异常而崩溃。这就是 .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 那么你应该毫无疑问地处理它。

于 2013-01-08T17:59:16.427 回答
3

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

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

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

因此,收集 System.Timers.Timer 垃圾需要调用它的 Stop() 方法或将其 Enabled 属性设置为 false,同样的事情。这会导致 CLR 从活动计时器列表中删除系统计时器。这也删除了对状态对象的引用。然后使计时器对象符合收集条件。

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

于 2013-01-08T14:51:04.020 回答
2

我认为这与 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。

这不是一个明确的答案,因为我发布的代码都没有创建这样的参考;但它的胆量足够复杂,让我怀疑这样的事情正在发生。

如果你有 Reflector(或类似的)看看,你就会明白我的意思。:)

于 2013-01-08T13:50:30.563 回答
1

因为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;
于 2013-01-08T13:37:53.367 回答
0

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

c = null;

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

于 2013-01-08T13:39:45.457 回答
0

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

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

现在让我们回到内部包装了 Threading.Timer 的 System.Timers.Timer。注意,前者构造后者时,使用了System.Timers.Timer成员方法,所以不能GCed System.Timers.Timer对象。

于 2015-11-13T08:23:34.910 回答