Pengenalan GC

Generasi, alokasi, dan disiplin disposal.

Daftar isi

Topik ini membahas cara kerja generation (generasi) pada garbage collector (GC) beserta disiplin disposal (pembebasan sumber daya secara eksplisit) untuk sumber daya non-memori. Pembahasan mencakup pembagian generation 0, 1, dan 2, pengamatan perilakunya lewat API GC, kebiasaan alokasi yang menjaga program tetap cepat, serta pola using untuk objek yang mengimplementasikan IDisposable. Setelah topik ini diselesaikan, perilaku GC dapat dibaca langsung dari keluaran kode demo dan sumber daya seperti berkas serta koneksi jaringan dapat ditutup secara andal.

Generation 0, 1, dan 2#

GC .NET membagi objek ke dalam tiga kelompok umur yang disebut generation, yaitu generation 0, 1, dan 2. Pembagiannya berdiri di atas pengamatan bahwa kebanyakan objek berumur sangat pendek, sehingga memeriksa objek muda lebih dulu menghasilkan ruang memori yang besar dengan biaya yang kecil. Koleksi generation 0 adalah yang termurah dan paling sering berjalan, sedangkan koleksi generation 2 memeriksa seluruh heap dan paling jarang terjadi.

Objek yang selamat dari satu koleksi naik satu generation, dan objek yang mencapai generation 2 dianggap berumur panjang. Perpindahannya satu arah, sehingga objek tidak pernah turun generation. Gambar berikut memperlihatkan alur objek baru sampai menjadi objek berumur panjang.

objek baruGeneration 0diperiksa paling seringselamat koleksiGeneration 1diperiksa lebih jarangselamat koleksiGeneration 2diperiksa paling jarang
Pembagian generation pada garbage collector: objek baru masuk ke generation 0 dan naik satu tingkat setiap kali selamat dari satu koleksi
Efek praktisnya adalah ketimpangan frekuensi pemeriksaan. Objek berumur pendek mendapat kesempatan dibersihkan cepat sebelum menumpuk, sedangkan objek berumur panjang jarang diperiksa karena dianggap tetap hidup. Koleksi generation yang lebih tinggi juga memeriksa generation di bawahnya, sehingga koleksi generation 1 menghitung koleksi generation 0 sekaligus.

Mengamati generation lewat kode#

Kelas GC menyediakan dua metode yang membuat perilaku di atas dapat diamati. Metode GC.GetGeneration melaporkan generation tempat sebuah objek berada, sedangkan GC.CollectionCount melaporkan berapa kali satu generation telah dikoleksi. Pada demo berikut, GC.Collect dipanggil secara eksplisit agar waktunya pasti, sehingga keluarannya dapat direproduksi persis.

csharp Program.cs
// Perilaku generasi garbage collector yang dapat diamati secara deterministik.
object data = new object();
Console.WriteLine($"Objek baru             : generation {GC.GetGeneration(data)}");

GC.Collect(0);
Console.WriteLine($"Setelah GC.Collect(0)  : generation {GC.GetGeneration(data)}");
GC.Collect(1);
Console.WriteLine($"Setelah GC.Collect(1)  : generation {GC.GetGeneration(data)}");

Console.WriteLine($"Koleksi generation 0   : {GC.CollectionCount(0)} kali");
Console.WriteLine($"Koleksi generation 1   : {GC.CollectionCount(1)} kali");
Console.WriteLine($"Koleksi generation 2   : {GC.CollectionCount(2)} kali");

GC.Collect();
Console.WriteLine($"Setelah GC.Collect()   : {GC.CollectionCount(2)} koleksi generation 2");
text Keluaran
Objek baru             : generation 0
Setelah GC.Collect(0)  : generation 1
Setelah GC.Collect(1)  : generation 2
Koleksi generation 0   : 2 kali
Koleksi generation 1   : 1 kali
Koleksi generation 2   : 0 kali
Setelah GC.Collect()   : 1 koleksi generation 2

Setiap baris keluaran memperlihatkan satu aturan yang sudah dibahas. Objek baru berada di generation 0, lalu naik ke generation 1 setelah selamat dari GC.Collect(0) dan ke generation 2 setelah selamat dari GC.Collect(1). Penghitung koleksi menegaskan ketimpangan frekuensinya: GC.Collect(1) menghitung satu kali pada generation 1 dan sekaligus satu kali pada generation 0, sehingga penghitung generation 0 mencatat dua kali. Pemanggilan GC.Collect() tanpa argumen mengumpulkan seluruh generation, dan baris terakhir menunjukkan penghitung generation 2 naik dari nol menjadi satu.

Alokasi dan disiplin disposal#

Mengalokasikan objek baru pada .NET tergolong murah, karena alokator hanya melangkahkan penunjuk memori pada generation termuda. Harga sebenarnya muncul kemudian, yaitu saat koleksi berjalan dan saat objek yang selamat naik generation. Karena objek di generation 2 jarang diperiksa, referensi yang ditahan lebih lama dari yang diperlukan membuat memori bertahan lama tanpa pernah dilepaskan. Kebiasaan yang menjaga program tetap cepat berdiri di sisi alokasinya: buat objek dalam cakupan yang pendek, gunakan ulang buffer pada jalur yang panas, dan jangan menahan referensi yang sudah tidak dipakai.

Disiplin yang lain menyangkut sumber daya yang bukan memori. GC hanya mengurus memori terkelola, sehingga berkas yang terbuka, koneksi jaringan, dan pegangan sumber daya sistem tidak otomatis ditutup olehnya. Tipe-tipe seperti itu mengimplementasikan antarmuka IDisposable dengan satu metode Dispose yang menutup sumber dayanya, dan pemanggilannya dititipkan kepada pernyataan using agar tidak terlupa. Bentuk using declaration (pernyataan using tanpa kurung kurawal) memanggil Dispose otomatis di akhir cakupan variabelnya, sebagaimana demokan berikut.

csharp Contoh: disposal otomatis dengan using declaration
// using declaration: metode Dispose dipanggil otomatis di akhir cakupan.
using CatatanSesi sesi = new CatatanSesi("sensor");

Console.WriteLine("Pengukuran suhu berjalan");
Console.WriteLine("Program selesai");

class CatatanSesi : IDisposable
{
    public CatatanSesi(string nama)
    {
        Nama = nama;
        Console.WriteLine($"CatatanSesi '{nama}' dibuka");
    }

    public string Nama { get; }

    public void Dispose() => Console.WriteLine($"CatatanSesi '{Nama}' ditutup lewat Dispose");
}
text Keluaran
CatatanSesi 'sensor' dibuka
Pengukuran suhu berjalan
Program selesai
CatatanSesi 'sensor' ditutup lewat Dispose

Urutan keluarannya menjelaskan poin utamanya. Pesan penutupan muncul terakhir, padahal tidak ada satu baris pun yang memanggil Dispose secara eksplisit, karena kompilator menyisipkan pemanggilan itu di akhir cakupan program. Pola ini memisahkan dua urusan yang berbeda: GC mengurus memori, sedangkan using dan Dispose mengurus sumber daya non-memori yang waktunya penutupan harus pasti.

Kesalahan umum#

  • Mengandalkan GC untuk menutup berkas atau koneksi. GC melepaskan memori pada waktu yang tidak pasti, sedangkan berkas yang terbuka tetap menahan pegangan sistem sampai Dispose dipanggil. Tutup sumber daya non-memori dengan pernyataan using dan jangan menunggu koleksi berjalan.
  • Menggunakan objek setelah Dispose dipanggil. Banyak tipe membangkitkan ObjectDisposedException ketika metodenya dipakai setelah penutupan. Jaga variabelnya tetap berada di dalam cakupan using sehingga urutan penggunaannya jelas.
  • Menahan referensi lebih lama dari yang diperlukan. Objek yang masih dirujuk tidak pernah dibersihkan, dan objek yang selamat naik generation lalu jarang diperiksa. Batasi cakupan variabel dan kosongkan field yang tidak lagi dipakai agar objeknya dapat dilepaskan.

Ringkasan#

  • GC membagi objek ke generation 0, 1, dan 2, memeriksa generation termuda paling sering, dan menaikkan satu generation setiap objek yang selamat satu koleksi.
  • GC.GetGeneration dan GC.CollectionCount membuat perilaku itu dapat diamati secara deterministik pada kode demo.
  • Koleksi generation yang lebih tinggi memeriksa generation di bawahnya, sehingga GC.Collect(1) menambah penghitung generation 0 sekaligus generation 1.
  • GC hanya mengurus memori, sedangkan sumber daya non-memori ditutup lewat IDisposable dan pernyataan using yang memanggil Dispose di akhir cakupan.

Referensi#