.NET Bellek Yönetimi: Çöp Toplayıcı (GC) Mekanizması.

.NET çalışma zamanı (CLR), geliştiricileri manuel bellek yönetiminden (malloc/free) kurtaran otomatik bir Çöp Toplayıcıya (Garbage Collector - GC) sahiptir. Bu yazıda, GC’nin çalışma prensipleri (Generasyonlar, İşaretle-ve-Temizle, Kökler), Stack (Yığın) ve Heap (Yığın) arasındaki temel farklar, Yönetilen (Managed) ve Yönetilmeyen (Unmanaged) kaynakların temizliği için IDisposable arayüzünün doğru kullanımı, Large Object Heap (LOH) tehlikeleri ve bellek sızıntısına (Memory Leak) yol açan yaygın

.NET Bellek Yönetimi: Çöp Toplayıcı (GC) Mekanizması.

.NET uygulamalarının performansını anlamanın anahtarı, bellek yönetimini derinlemesine kavramaktan geçer. CLR (Common Language Runtime), bellek yönetimini iki temel alan üzerine kurar: Stack (Yığın) ve Managed Heap (Yönetilen Yığın).

  • Stack (Yığın): Değer tipleri (int, bool, struct) ve metot çağrılarına ait referanslar burada tutulur. Thread başına ayrılan bu bölge LIFO (Son Giren İlk Çıkar) mantığıyla çalışır ve metot sonlandığında otomatik olarak temizlenir. Yönetim maliyeti neredeyse sıfırdır.

  • Managed Heap (Yönetilen Yığın): Referans tipleri (class, string, array) burada yaşar. İşte asıl karmaşıklık ve GC bu devreye girer.

Çöp Toplayıcının (GC) Çalışma Mantığı
GC, bir "arka plan işçisi" gibi çalışır ve çalıştırma zamanı (Runtime) tarafından ihtiyaç duyulduğunda tetiklenir. Temel mantığı "Köklerden (Roots) Ulaşılabilirlik" üzerinedir: Uygulamanın çalışan thread'leri üzerinden erişilemeyen (kökleri olmayan) tüm nesneler çöptür ve toplanmaya adaydır.

GC, Mark (İşaretle), Sweep (Temizle) ve Compact (Sıkıştırma) aşamalarından oluşur. Performansı optimize etmek için nesneleri Generasyon (Kuşak) adı verilen 3 mantıksal bölüme ayırır:

  • Gen 0 (Genç Nesneler): Yeni oluşturulan nesneler. Sık sık taranır ve toplanır. Çok hızlıdır.

  • Gen 1 (Ara Kuşak): Gen 0'dan bir çöp toplama işlemini atlatmayı başarmış nesneler için tampon bölgedir.

  • Gen 2 (Uzun Ömürlü Nesneler): Uygulama hayatı boyunca yaşayacak statik veya uzun süreli nesneler. En az sıklıkla taranır.

Bunlara ek olarak Large Object Heap (LOH) vardır. 85.000 byte’tan (veya .NET Core’da array tipine göre değişken) büyük nesneler doğrudan Gen 2'ye atanır ve Compact (Sıkıştırma) yapılmaz (sadece Sweep yapılır) çünkü büyük nesneleri taşımak maliyetlidir. Bu nedenle LOH üzerinde art arda büyük nesne tahsis etmek bellek parçalanmasına (Fragmentation) ve OutOfMemoryException hatalarına yol açar.

Kaynak Yönetimi: IDisposable ve Finalize (Sonlandırıcı)
GC yalnızca yönetilen (managed) belleği (RAM) temizler. Ancak dosya tanıtıcıları (FileHandle), veritabanı bağlantıları (SqlConnection), GDI+ objeleri veya TCP soketleri yönetilmeyen (unmanaged) kaynaklardır. GC bunları doğrudan temizleyemez. İşte bu noktada IDisposable arayüzü devreye girer.

  • Dispose Metodu: Geliştiricinin kaynağı hemen serbest bırakması için kullanılır. using bloğu ile kullanılması zorunludur.

  • Finalizer (Sonlandırıcı / ~ClassName): Eğer geliştirici Dispose'ı çağırmayı unutursa, GC nesneyi toplarken Finalize metodunu çağırır. Ancak bu, nesnenin ömrünü uzatır (GC, nesneyi Finalization kuyruğuna alır) ve performansı ciddi oranda düşürür. Bu nedenle "Dispose Pattern" uygulanarak Finalizer sadece bir emniyet subabı olarak kullanılmalıdır.

En Yaygın Bellek Sızıntısı (Memory Leak) Sebepleri
.NET'te "Memory Leak" terimi genelde RAM’in işletim sistemine geri verilmemesinden değil, "Gereksiz yere hayatta tutulan nesne referansları" nedeniyle oluşur. Başlıca sebepler:

  1. Statik Olaylara (Static Events) Abone Olmak: Bir nesne bir static event'e abone olduğunda, o event nesnenin referansını tutar. Nesne dispose edilse bile event iptal edilmediği sürece GC nesneyi toplamaz. (Unutmayın: Abone olduysanız, aboneliği iptal edin!)

  2. Statik Koleksiyonlar: static List<Customer> gibi yapılar, uygulama kapanana kadar eklenen tüm nesneleri canlı tutar.

  3. Kapalı Lambda ve Delegate’ler: Bir metot içinde tanımlanan lambda ifadeleri, eğer dışarıdaki bir değişkeni (capture) yakalarsa, bu değişkenin ömrünü uzatır.

Performans İpuçları ve Diagnostik

  • GC.TryStartNoGCRegion kullanarak kritik anlık işlemlerde GC’yi geçici olarak durdurabilirsiniz.

  • Workstation GC (UI uygulamaları için duyarlı) ve Server GC (Çok çekirdekli sunucularda throughput için) modları arasında proje dosyanızdan seçim yapabilirsiniz.

  • Bellek sorunlarını tespit etmek için dotMemory, PerfView veya Visual Studio Diag araçlarını kullanın. Özellikle PerfView, GC istatistiklerini ve hangi nesnelerin ne kadar süre yaşadığını detaylı gösterir.

  • ArrayPool ve Object Pooling (Örneğin Microsoft.Extensions.ObjectPool) kullanarak özellikle büyük ve sık oluşturulan nesnelerin tahsis maliyetini düşürebilirsiniz.

Özetle: .NET bellek yönetimi "Kullan ve Unut" kolaylığı sunsa da, referans zincirlerini, LOH tahsislerini ve yönetilmeyen kaynakların temizliğini ihmal etmek, uygulamanızın zamanla yavaşlamasına veya çökmesine neden olur. İyi bir .NET geliştiricisi, GC'nin nasıl çalıştığını bilerek kod yazar ve bellek profilini düzenli olarak analiz eder.

Tüm yazılar