本做法討論幾個與 GC 相關的基本知識: Managed
、Unmanaged
、Finalizer
GC 能夠幫我們管理 Managed
記憶體讓我們不必關心記憶體洩漏這種記憶體管理的問題,大多數情況讓 GC 自行運作比你自己手動清理做的更好。
GC 釋放記憶體的首要步驟就是檢查 application's roots 也就是應用程式的根物件包含以下種類:
- static fields
- local variables
- thread's stack
- CPU registers
- GC handles
- finalize queue
GC 會使用 Mark-Compact 演算法將那些 reachable 的物件標記起來,標記起來的用意是代表這個物件還有人在參考中所以不能清除掉, 這時候建立完標記的物件可能散落在 Managed Heap 中,因此第二階段的壓縮就是把這些標記物件重新移動位置,這樣既能讓這些還需要使用的 物件放在一段連續記憶體當中,也能把閒置的空間騰出來。
但是在 .NET 中有還有許多 Unmanaged
資源需要我們自行管理,所以可能讓這些資源留在記憶體內過久導致問題,
.NET 有兩種機制可以釋放這種資源分別是 finalizer
與 IDisposable
。
也就是像下面這種寫法,Finalizer
也能叫做 Destructors
與 Constructors
要做的事情相反,它的責任是把物件進行銷毀。
class Car
{
~Car() // finalizer
{
// cleanup statements...
}
}
這個 Finalizer
寫法在 C++ 中是很重要的寫法,但是在 .NET 中需要注意使用的時機,還記的上面的根物件裡面有一個 finalize queue
嗎?
只要你寫的 class 裡面有 Finalizer
那麼就會排入到這個佇列中等待之後釋放階段執行,也就是說這個釋放的機制並不是及時的更有可能影響 GC。
舉例來說下面我建立一公開屬性並初始化成 1,並且我有建立一個空的 finalizer
,這邊原本對 GC 來說是一件很簡單的工作,但是因為加了 finalizer
變成流程要多一個步驟去執行這個空的 finalizer
,所以這種情況反而會影響效能。
public class MyClass
{
public int MyProperty { get; set; } = 1;
public MyClass() {}
~MyClass() {}
}
所以現在比較建議的做法是實做 IDisposable
,但是缺點是呼叫方一定要自行呼叫 Dispose
方法否則就不會釋放,在 C# 中常用的做法是使用 using,
這樣背後就會自動產生 try/finally
與呼叫 Dispose
方法。
void Main()
{
using(var c = new MyClass())
{
}
}
public class MyClass : IDisposable
{
public void Dispose()
{
}
}
產生出來的程式碼會長得像這樣,把 Dispose
方法包在 finally 裡面,這樣釋放階段就會自動運行 Dispose
方法。
MyClass c = new MyClass ();
try
{
}
finally
{
if (c != null)
{
((IDisposable)c).Dispose();
}
}
最後就是將 finalizer
與 IDisposable
結合起來的寫法,這樣可以避免使用者沒有呼叫 Dispose
方法也沒有使用 using
,
在這種最糟情況下留下一個自動釋放的手段。
class BaseClass2 : IDisposable
{
// Flag: Has Dispose already been called?
bool disposed = false;
// Public implementation of Dispose pattern callable by consumers.
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
// Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing)
{
if (disposed)
return;
if (disposing)
{
// Free any other managed objects here.
}
// Free any unmanaged objects here.
disposed = true;
}
~BaseClass2()
{
Dispose(disposing: false);
}
}
Summary
在這個做法中學了基礎的 GC 回收記憶體的流程與 finalizer
與 IDisposable
的使用方法,還有 finalizer
可能會導致性能損失,
雖然 IDisposable
是比較推薦的做法,但是沒有經驗的工程師可能會忘記使用 using 來釋放物件,所以將 finalizer
與 IDisposable
結合起來看起來是比較好的做法。