【问题标题】:Why does a Timer keep my object alive?为什么计时器让我的对象保持活力?
【发布时间】:2012-12-22 08:55:08
【问题描述】:

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

众所周知,添加事件处理程序会导致 C# 中的内存泄漏。见Why and How to avoid Event Handler memory leaks?

另一方面,对象通常具有相似或相互关联的生命周期,因此没有必要取消注册事件处理程序。考虑这个例子:

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 使对象保持活动状态?计时器是否通过计时器机制的某些“隐藏”引用(例如静态引用)保持活动状态?

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

【问题讨论】:

  • 我也遇到了这个问题,我的解决方案是在不再需要计时器时停止计时器,然后您的 C 类将被垃圾收集。
  • 我真的只是停止阅读另一种语言的主题,其中计时器在 iPad 上保持对象处于活动状态,他们只是确保在完成计时器并释放对象后处理掉它也可以丢弃。
  • 值得注意的是,它与C类对定时器的引用无关,尝试删除该字段,只使用一个局部变量作为定时器。

标签: c# events timer


【解决方案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;

【讨论】:

    【解决方案2】:

    我认为问题出在这一行;

    c = null;
    

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

    【讨论】:

    • 当然将其设置为 null 不会导致对象被删除。但它删除了对对象的唯一(明显)引用,因此手动触发 GC 应该导致对象被删除
    • 这可能是正确的,如果它没有被 Timer 引用。计时器仍然引用它。
    • 但它是一个循环引用,所以我认为它应该得到 GC'd。计时器和 C 类都没有外部引用。无论如何,我的解决方案是停止计时器,但也许删除事件处理程序也可以。
    • 我认为,计时器对象仍在被某些系统对象引用;因此它在停止之前不会被删除。 windbg 可能有用 (blogs.msdn.com/b/delay/archive/2009/03/11/…)
    【解决方案3】:

    我认为这与 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(或类似的)看看,你就会明白我的意思。 :)

    【讨论】:

    • 如果您按照代码进行操作,您最终会得到一个本机方法调用,该方法调用将回调作为参数传递,因此“本机的东西”持有对回调的引用。
    • 啊,大概就是这样。
    【解决方案4】:

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

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

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

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

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

    【讨论】:

      【解决方案5】:

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

      操作系统 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 那么你应该毫无疑问地处理它。

      【讨论】:

        【解决方案6】:

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

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多