GCHandle Leak

iDotNetSpace發表於2009-08-11

剛剛在園子裡看到一篇文章《.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後呢?為什麼?
GCHandle LeakCode

我們分析一下問題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的原始碼吧,看看原始碼中到底是怎麼回事
GCHandle Leak
    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程式碼中做的,不便於除錯。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-611887/,如需轉載,請註明出處,否則將追究法律責任。