Effective C# 11.認識 .NET 資源管理 Effective C# 11.認識 .NET 資源管理 (Understand .NET Resource Management)

Published on Sunday, October 6, 2024

本做法討論幾個與 GC 相關的基本知識: ManagedUnmanagedFinalizer GC 能夠幫我們管理 Managed 記憶體讓我們不必關心記憶體洩漏這種記憶體管理的問題,大多數情況讓 GC 自行運作比你自己手動清理做的更好。

GC 釋放記憶體的首要步驟就是檢查 application's roots 也就是應用程式的根物件包含以下種類:

  1. static fields
  2. local variables
  3. thread's stack
  4. CPU registers
  5. GC handles
  6. finalize queue

GC 會使用 Mark-Compact 演算法將那些 reachable 的物件標記起來,標記起來的用意是代表這個物件還有人在參考中所以不能清除掉, 這時候建立完標記的物件可能散落在 Managed Heap 中,因此第二階段的壓縮就是把這些標記物件重新移動位置,這樣既能讓這些還需要使用的 物件放在一段連續記憶體當中,也能把閒置的空間騰出來。

但是在 .NET 中有還有許多 Unmanaged 資源需要我們自行管理,所以可能讓這些資源留在記憶體內過久導致問題, .NET 有兩種機制可以釋放這種資源分別是 finalizerIDisposable

也就是像下面這種寫法,Finalizer 也能叫做 DestructorsConstructors 要做的事情相反,它的責任是把物件進行銷毀。

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();
     }
}

最後就是將 finalizerIDisposable 結合起來的寫法,這樣可以避免使用者沒有呼叫 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 回收記憶體的流程與 finalizerIDisposable 的使用方法,還有 finalizer 可能會導致性能損失, 雖然 IDisposable 是比較推薦的做法,但是沒有經驗的工程師可能會忘記使用 using 來釋放物件,所以將 finalizerIDisposable 結合起來看起來是比較好的做法。