GCHandle Leak

刚刚在园子里看到一篇文章《.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();
        }

问题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代码中做的,不便于调试。
请使用浏览器的分享功能分享到微信等