Bab Finalizer di section Memory Management melihat finalizer dari sisi pemakaian API; bab ini membongkar mekanismenya dan menetapkan aturan pola Dispose. Finalization adalah proses dua tahap: saat GC menemukan objek berfinalizer yang tidak dicakup rujukan lagi, GC tidak langsung mengklaim memorinya. Runtime memindahkan entri objek ke antrean siap-eksekusi (F-Reachable queue), thread finalizer menjalankan method Finalize, dan barulah pada siklus GC berikutnya memori objek benar-benar diklaim.

Contoh: Membuktikan Pengaruh SuppressFinalize #

csharp
var denganDispose = new LogAktivitas(akanDiDispose: true);
denganDispose.Dispose();

BuatTanpaDispose();

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Console.WriteLine($"Finalizer setelah Dispose: {LogAktivitas.DenganDispose}");
Console.WriteLine($"Finalizer tanpa Dispose  : {LogAktivitas.TanpaDispose}");

void BuatTanpaDispose()
{
    var log = new LogAktivitas(akanDiDispose: false);
    _ = log;
}

class LogAktivitas(bool akanDiDispose) : IDisposable
{
    public static bool DenganDispose { get; private set; }
    public static bool TanpaDispose { get; private set; }

    private readonly bool _akanDiDispose = akanDiDispose;

    ~LogAktivitas()
    {
        if (_akanDiDispose)
            DenganDispose = true;
        else
            TanpaDispose = true;
    }

    public void Dispose() => GC.SuppressFinalize(this);
}

Output yang Dihasilkan:

text
Finalizer setelah Dispose: False
Finalizer tanpa Dispose  : True

Mari kita bedah baris demi baris:

  1. denganDispose.Dispose(): Dispose memanggil GC.SuppressFinalize(this), yaitu memerintahkan runtime untuk tidak memasukkan objek ke antrean finalizer.
  2. BuatTanpaDispose(): objek kedua dibuat tanpa Dispose; ketika method kembali tidak ada lagi rujukan kuat ke objek tersebut.
  3. GC.WaitForPendingFinalizers(): menunggu seluruh finalizer yang sudah masuk antrean selesai dieksekusi; thread finalizer lalu menandai TanpaDispose menjadi True.
  4. Dua baris laporan: False membuktikan finalizer pada objek yang di-Dispose tidak pernah berjalan, dan True membuktikan finalizer tetap berjalan untuk objek yang tidak di-Dispose.

Aturan Pola Dispose #

  • Class dengan finalizer sebaiknya sealed, atau pola Dispose(bool) dipakai agar class turunan ikut tertata.
  • Dispose wajib memanggil GC.SuppressFinalize(this) supaya objek tidak selamat dua siklus GC tanpa guna.
  • Badan finalizer hanya menyentuh resource non-managed; melepas objek managed di finalizer justru menunda pengklaimannya.
  • Panggil Dispose lebih dulu; finalizer hanyalah jaring pengaman, bukan jalur utama.

Urutan eksekusi antar finalizer tidak dijamin. Runtime memasukkan entri ke antrean sesuai urutan penemuan saat penelusuran, bukan urutan pembuatan objek, sehingga finalizer tidak boleh menyentuh objek managed lain yang juga berfinalizer. Objek tersebut mungkin telah dieksekusi lebih dulu, dan mengasumsikan kondisinya berarti mengandalkan perilaku yang tidak pernah dijanjikan runtime.