刚刚在园子里看到一篇文章《.net中的游魂现象》,正好这两天关心这个问题,就打算再写篇文章和大家讨论一下,先给大家提两个问题:
问题1:点击button1后,Timer会被GC回收吗?点击button2后呢?为什么?(这个问题来自《.net中的游魂现象》这篇文章中,不过便有分析,Timer现在是System.Windows.Forms.Timer)。
private void button1_Click(object sender, EventArgs e)
{
System.Windows.Forms.Timer t = new System.Windows.Forms.Timer();
t.Interval = 1000 * 2;
t.Tick+=delegate{
Console.WriteLine(DateTime.Now.ToString());
};
t.Start();
}
private void button2_Click(object sender, EventArgs e)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
{
System.Windows.Forms.Timer t = new System.Windows.Forms.Timer();
t.Interval = 1000 * 2;
t.Tick+=delegate{
Console.WriteLine(DateTime.Now.ToString());
};
t.Start();
}
private void button2_Click(object sender, EventArgs e)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
问题2:在点击button1后,Form2会被GC回收吗?点击button2后呢?为什么?
Code
我们分析一下问题1,实际上问题2也是类似的问题,只不过更难分析罢了,有兴趣你也调试一下吧。
附件:TestApp.rar
以下是我的调试过程:
一,使用SOS分析内存中的状态。
1,点击button1, 执行完成后点击几次button2,执行垃圾回收,然后在button2_Click开始处下断点,使用SOS分析一下现在内存状态。
2,加载sos.dll
.load C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll
extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded
3,dump堆中System.Windows.Forms.Timer类型的对象
!dumpheap -type System.Windows.Forms.Timer
PDB symbol for mscorwks.dll not loaded
Address MT Size
01ec25cc 602ccc44 48
01ec2628 602d7b9c 52
total 2 objects
Statistics:
MT Count TotalSize Class Name
602ccc44 1 48 System.Windows.Forms.Timer
602d7b9c 1 52 System.Windows.Forms.Timer+TimerNativeWindow
Total 2 objects
发现有一个System.Windows.Forms.Timer存活,可见,Timer并没有被垃圾回收。
4,使用GCHandle查看当前进程存活的GCHandles。
!GCHandles
GC Handle Statistics:
Strong Handles: 51
Pinned Handles: 7
Async Pinned Handles: 0
Ref Count Handles: 0
Weak Long Handles: 15
Weak Short Handles: 68
Other Handles: 0
Statistics:
MT Count TotalSize Class Name
617f0770 1 12 System.Object
60f926d8 1 12 System.Diagnostics.TraceListenerCollection
617f1cd4 1 20 System.RuntimeType
617f1220 1 28 System.SharedStatics
60f8246c 1 32 Microsoft.Win32.NativeMethods+WndProc
60f822e4 1 32 Microsoft.Win32.NativeMethods+ConHndlr
00126f3c 1 32 Microsoft.VisualStudio.Debugger.Runtime.Main+_ThrowCrossThreadMessageException
60f9ccdc 1 40 System.Diagnostics.BooleanSwitch
602ccc44 1 48 System.Windows.Forms.Timer
...
Total 141 objects
当我在使用GCHandles查看进程中存活的GCHandles后,发现Timer居然被GCHandle抓着,在.NET的垃圾回收机制中,强GCHandle是不会被垃圾回收器收集的,它们被认为是垃圾收集中引用的root对象,由这些对象抓着的子对象也不会被收集,只有弱GCHandle才能被垃圾回收器收集。关于GCHandle,可以查看MSDN-GCHandleType枚举。现在问题明白了,问题1中的代码存在GCHandle泄露,在调用GCHandle.Alloc方法后,没有调用GCHandle.Free方法释放它。
二,使用Reflector看看System.Windows.Forms.Timer的源代码吧,看看源代码中到底是怎么回事
public class Timer : Component
{
public virtual bool Enabled
{
get
{
if (this.timerWindow == null)
{
return this.enabled;
}
return this.timerWindow.IsTimerRunning;
}
set
{
lock (this.syncObj)
{
if (this.enabled != value)
{
this.enabled = value;
if (!base.DesignMode)
{
if (value)
{
if (this.timerWindow == null)
{
this.timerWindow = new TimerNativeWindow(this);
}
this.timerRoot = GCHandle.Alloc(this);
this.timerWindow.StartTimer(this.interval);
}
else
{
if (this.timerWindow != null)
{
this.timerWindow.StopTimer();
}
if (this.timerRoot.IsAllocated)
{
this.timerRoot.Free();
}
}
}
}
}
}
}
public void Start()
{
this.Enabled = true;
}
public void Stop()
{
this.Enabled = false;
}
}
在System.Windows.Forms.Timer的Enable属性的代码中,我们可以清楚的看到如果设置Enable=true,则Timer会被强GCHandle抓着,如果设成false,才能够调用到GChandle.Free()方法释放它。
所以在例一的代码中,点击button1后,Timer不会被垃圾回收,点击button2后,Timer也不会不垃圾回收。如果想要Timer被垃圾回收,必须调用Enable=false或等价代码,如调用Stop方法,Dispose方法等。
例二其实也是同样的问题,有兴趣的朋友可以分析一下。《.net中的游魂现象》这篇文章中的代码也是同样的问题,可以通过GCHandle看到System.Threading._TimerCallback被GCHandle抓着没有释放,只不过这是在Unmanaged代码中做的,不便于调试。