为什么一个计时器让我的对象还活着吗?(Why does a Timer keep my object

2019-07-18 00:19发布

前言 :我知道如何解决这个问题。 我想知道为什么会发生。 请阅读从顶部到底部的问题。

大家都(应该)知道,添加事件处理可能会导致在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对象? 为什么定时器保持对象还活着吗? 是定时器由定时器力学的一些“隐藏”引用(例如静态基准)保持活力?

(*)注意:如果只创建了计时器,没有开始,也不会发生问题。 如果它的开始,后来停止了,但事件处理函数不是注销,问题仍然存在。

Answer 1:

定时器逻辑依赖于OS的功能。 它实际上是引发事件的OS。 OS反过来使用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

然后,如果你看看像dotPeek的System.Threading.TimerQueue类,你会看到,它是作为一个单独实现的,它拥有定时器的集合。

这就是它是如何工作的。 不幸的是,MSDN文档不是一清二楚了。 他们想,如果它实现IDisposable,那么你应该处理它毫无疑问的问。



Answer 2:

是定时器由定时器力学的一些“隐藏”引用(例如静态基准)保持活力?

是。 它是建立在CLR中,你可以看到它的痕迹,当您使用的参考源或反编译,私营的“cookie”字段中Timer类。 它是作为第二个参数,以实际实现的定时器,在“状态”对象System.Threading.Timer构造函数。

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

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

显然,这是可取的行为,则通常不希望有一个计时器只是消失和停止滴答作响,而它是积极的。 当您使用System.Threading.Timer这发生,也停止调用它的回调,如果你不参考保持到它,无论明示或使用状态对象。



Answer 3:

我认为这是相关的定时器的实现方式。 当你调用Timer.Start(),它设置Timer.Enabled =真。 看看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.Stop()这样的时间或Timer.Enabled =假。

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

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



Answer 4:

Because Timer is still active. (Event handler is not removed for Timer.Elapsed).

If you want to properly dispose, Implement IDisposable interface, remove the event handler in the Dispose method, and use the using block or call the Dispose manually. The issue will not occur.

Example

 public class C : IDisposable  
 {
    ...

    void Dispose()
    {
      timer.Elapsed -= timer_elapsed;
    }
 }

And then

 C c = new C();

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


Answer 5:

我认为,来自该行的问题出现;

c = null;

在一般情况下,大多数开发商认为,做一个对象等于空的对象结果被垃圾收集器删除。 但这种情况并非如此; 实际上只有一个存储位置(在其中创建对象c)的参考被删除; 如果有对相关的记忆位置的任何其他引用,对象就不会被标记为删除。 在这种情况下,由于Timer正在引用相关的存储器位置,对象将不会被垃圾收集器中删除。



Answer 6:

让我们先说说Threading.Timer。 在内部,定时器将构建使用回调状态的TimerQueueTimer对象传递给定时器的ctor(说新Threading.Timer(回调状态,XXX,XXX)的TimerQueueTimer将被添加到一个静态列表。

如果回调方法和状态都没有“这个”信息(说使用回调和空的状态的静态方法),那么计时器对象可以是GCed时没有提及。 在另一方面,如果使用回调的成员方法,含有该委托“这个”将被存储在上述的静态列表。 因此Timer对象不能GCed因为“C”(在你的例子)对象仍引用。

现在,让我们回System.Timers.Timer其内部包装Threading.Timer。 注意,当前者构建后者中,使用System.Timers.Timer构件的方法,所以System.Timers.Timer对象不能GCed。



文章来源: Why does a Timer keep my object alive?
标签: c# events timer