C# 定時器保活機制引起的記憶體洩露問題
C# 中有三種定時器,System.Windows.Forms
中的定時器和 System.Timers.Timer
的工作方式是完全一樣的,所以,這裡我們僅討論 System.Timers.Timer
和 System.Threading.Timer
1、定時器保活
先來看一個例子:
class Program { static void Main(string[] args) { Start(); GC.Collect(); Read(); } static void Start() { Foo f = new Foo(); System.Threading.Thread.Sleep(5_000); } } public class Foo { System.Timers.Timer _timer; public Foo() { _timer = new System.Timers.Timer(1000); _timer.Elapsed += timer_Elapsed; _timer.Start(); } private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { WriteLine("System.Timers.Timer Elapsed."); } ~Foo() { WriteLine("---------- End ----------"); } }
執行結果如下:
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
...
在 Start
方法結束後,Foo
例項已經失去了作用域,按理說應該被回收,但實際並沒有(因為解構函式沒有執行,所以肯定例項未被回收)。
這就是定時器的 保活機制,因為定時器需要執行 timer_Elapsed
Foo
例項,所以 Foo
例項被保活了。
但多數時候這並不是我們想要的結果,這種結果導致的結果就是 記憶體洩露,解決方案是:先將定時器 Dispose
。
public class Foo : IDisposable
{
...
public void Dispose()
{
_timer.Dispose();
}
}
一個很好的準則是:如果類中的任何欄位所賦的物件實現了IDisposable
介面,那麼該類也應當實現 IDisposable
介面。
在這個例子中,不止
Dispose
方法,Stop
方法和設定AutoReset = false
,都能起到釋放物件的目的。但是如果在Stop
方法之後又呼叫了Start
方法,那麼物件依然會被保活,即便Stop
之後進行強制垃圾回收,也無法回收物件。
System.Timers.Timer
和 System.Threading.Timer
的保活機制是類似的。
保活機制是由於定時器引用了例項中的方法,那麼,如果定時器不引用例項中的方法呢?
2、不保活下 System.Timers.Timer
和 System.Threading.Timer
的差異
要消除定時器對例項方法的引用也很簡單,將 timer_Elapsed
方法改成 靜態 的就好了。(靜態方法屬於類而非例項。)
改成靜態方法後再次執行示例,結果如下:
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
---------- End ----------
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
System.Timers.Timer Elapsed.
...
Foo
例項是被銷燬了(解構函式已執行,打印出了 End),但定時器還在執行,這是為什麼呢?
這是因為,.NET Framework 會確保 System.Timers.Timer
的存活,即便其所屬例項已經被銷燬回收。
如果改成 System.Threading.Timer
,又會如何?
class Program
{
static void Main(string[] args)
{
Start();
GC.Collect();
Read();
}
static void Start()
{
Foo2 f2 = new Foo2();
System.Threading.Thread.Sleep(5_000);
}
}
public class Foo2
{
System.Threading.Timer _timer;
public Foo2()
{
_timer = new System.Threading.Timer(timerTick, null, 0, 1000);
}
static void timerTick(object state)
{
WriteLine("System.Threading.Timer Elapsed.");
}
~Foo2()
{
WriteLine("---------- End ----------");
}
}
注意,這裡的 timerTick
方法是靜態的。執行結果如下:
System.Threading.Timer Elapsed.
System.Threading.Timer Elapsed.
System.Threading.Timer Elapsed.
System.Threading.Timer Elapsed.
System.Threading.Timer Elapsed.
---------- End ----------
可見,隨著 Foo2
例項銷燬,_timer
也自動停止並銷燬了。
這是因為,.NET Framework 不會儲存啟用 System.Threading.Timer
的引用,而是直接引用回撥委託