为什么Timer让我的对象存活

本文关键字:对象 我的 Timer 为什么 | 更新日期: 2023-09-27 17:50:57

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

我们都(应该)知道,在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保持对象存活?计时器是否通过某些"隐藏的"计时器机制引用(例如静态引用)保持活跃?

(*)注意:如果定时器只创建,没有启动,则不会出现此问题。如果它已启动并在稍后停止,但事件处理程序未注销,则问题仍然存在。

为什么Timer让我的对象存活

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

操作系统API,即Win32,不包含对任何类型的任何对象的引用。它保存了当计时器事件发生时必须调用的函数的内存地址。. net GC没有办法跟踪这种"引用"。因此,可以在不取消订阅低级事件的情况下收集计时器对象。这是一个问题,因为操作系统无论如何都会尝试调用它,并且会因为一些奇怪的内存访问异常而崩溃。这就是为什么。net框架将所有这样的计时器对象保存在静态引用对象中,并且只有当您取消订阅时才将它们从集合中删除。

如果你使用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

然后,如果你看一下System.Threading.TimerQueue类,比如dotPeek,你会发现它是作为一个单例实现的,并且它保存了一个计时器集合。

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

计时器是否通过某些"隐藏的"计时器机制引用(例如静态引用)保持活跃?

是的。它是在CLR中构建的,当您使用参考源或反编译器时,您可以看到它的跟踪,Timer类中的私有"cookie"字段。它作为第二个参数传递给实际实现计时器的System.Threading.Timer构造函数,即"state"对象。

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

所以要收集一个system . timer . timer垃圾需要你调用它的Stop()方法或者设置它的Enabled属性为false,同样的事情。这会导致CLR从活动计时器列表中删除系统计时器。它还删除了对state对象的引用。然后使计时器对象符合收集条件。

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

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

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。启用= false.

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

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

因为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。在内部,计时器将使用回调和状态(例如new Threading)来构造一个TimerQueueTimer对象。Timer(callback, state, xxx, xxx). TimerQueueTimer将被添加到一个静态列表中。

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

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