先前的篇章我們簡介了 C# .NET 中的垃圾回收機制,撰寫期間剛好簡訊相關的專案也出現了 Memory Leaks Bug……儘管垃圾回收機制為我們省下許多管理記憶體的心力,然而 GC 並不是萬靈丹,仍有許多程式的撰寫漏洞會造就 Memory Leaks,與時漸進,造成記憶體耗盡之後進而 Crash 整個程式。在今天的篇章中我們將介紹幾個常見 .NET 中造成 Memory Leaks 的原因。
在進入正題之前,我們首先需定義何謂 Memory Leaks。為何 GC 應該已經替我們管理記憶體空間了仍會有此問題?有兩個主要的原因,第一個是物件被參考之後但並未被經常使用(例如 C# 中某些語法糖實際上會創造一些物件),因為這些物件存在參考的關係,所以 GC 並不會去對它們進行回收的動作,這些物件會一直儲存在記憶體裡面消耗資源,最終導致記憶體耗盡導致錯誤產生;第二種可能性是開發者自行手動配置記憶體而非交給 GC 管理,但從未釋放這些空間。
1. 未將 Events Handler 解除
Event 是 .NET 中造成 Memory Leaks 的常見原因,一旦 subscribe 一個 Event,物件便會保存該 Class 的參考(除非該方法是匿名方法),如此一來 GC 便不會釋出該空間。
為避免此種狀況有幾種方法:記得對 Event 做 Unsubscribe、利用 Weak Event Patterns、盡量利用匿名函式並避免 capture 其他 members。
2.在匿名方法中
我們在前一項中提到避免 Memory Leaks 方法包括盡量利用匿名函式並避免 capture 其他 members,以下圖為例:
id 被放在匿名方法中呼叫,當 JobQueue 出現且參考委派事件,將同時參考到 MyClass 的實例;有個簡單的方法可避免此狀況,就是在 Foo() 函式中新增一個區域變數,並將 id 指派給該變數,在 Foo 函示中以該區域變數進行操作,便能迴避 Memory Leak 的潛在風險。
3.Static 變數
大多時候我們會避免使用太多 Static 變數,除了難以管理外,還跟垃圾回收機制的原理有關係:先前我們曾談到 GC 在掃描物件時是由 GC Root 出發(詳情可參照前幾篇關於垃圾回收的簡介篇章),GC Root 可以是正在方法裡執行的區域變數、 經由互動傳遞至非控管的 COM+ 函式庫的控管物件(Amanaged object is passed to an unmanaged COM+ library through interop)、具備 Finalizer 的物件、Static 變數。意即所有 Static 變數與其參考皆永遠不會被 GC 回收,永遠流在記憶體中,導致 Memory Leak。
4.不會終止的執行緒
我們在上面提到正在方法裡執行的區域變數也能充當 GC Root,更進一步地說,在存活中的執行緒 Call Stack 上的所有變數及成員皆能當作 GC Root。倘若我們建立一個會持續運行而不會終止、沒做其他動作但包括對某些物件參考的 Thread,由於記憶體資源持續被消耗,最終它也將引發 Memory Leak。
5.快取功能
在開發過程中我們常會將重複使用的資料儲存至快取中,避免浪費時間和效能在反覆存取資料庫上;然而若快取未設置淘汰時間,將會吃掉大量的記憶體,最終導致 Memory Leak。有幾種手段可以有效避免此狀況,例如設定清除快取的時限、設置快取空間限制、使用 WeakReference 類別:意即在參考物件的同時仍允許系統透過 GC 回收物件。
以上是幾種比較普遍的狀況,雖然看似容易理解,但觸發這些狀況的情境不一定如此直觀,有時會牽涉到 C# 語法、或是一些函式庫中的背後行為;在撰寫程式碼時,了解背後的運作原理將能更有效地進行問題的排除,希望大家都能完美迴避 Memory Leaks。
Reference
https://kudchikarsk.com/memory-leak-c/
https://www.red-gate.com/simple-talk/dotnet/.net-framework/understanding-garbage-collection-in-.net/
https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet/
https://docs.microsoft.com/zh-tw/dotnet/framework/wpf/advanced/weak-event-patterns