C# ve .NET [24]

C# ve .NET’te Garbage Collection (Çöp Toplama): Otomatik Bellek Yönetiminin Derinlikleri Giriş: Bellek Yönetiminin Zorluğu ve GC’nin Rolü Yazılım geliştirmenin en temel zorluklarından biri bellek yönetimidir. Uygulamalar çalışırken, çeşitli nesneler (veri yapıları, sınıf örnekleri vb.) oluşturmak için belleğe ihtiyaç duyarlar. Bu belleğin verimli bir şekilde ayrılması (allocation) ve artık ihtiyaç duyulmadığında geri serbest bırakılması (deallocation) kritik öneme sahiptir. Eğer bellek düzgün bir şekilde serbest bırakılmazsa, uygulama zamanla giderek daha fazla bellek tüketir ve sonunda kaynakların tükenmesiyle veya performans sorunlarıyla karşılaşır. Bu duruma bellek sızıntısı (memory leak) denir. Diğer yandan, hala kullanımda olan bir bellek alanı yanlışlıkla serbest bırakılırsa, bu durum dangling pointer veya use-after-free hatalarına yol açarak uygulamanın çökmesine veya öngörülemeyen davranışlar sergilemesine neden olabilir. C veya C++ gibi yönetilmeyen (unmanaged) dillerde, bellek yönetimi genellikle geliştiricinin sorumluluğundadır. Geliştiriciler malloc/free veya new/delete gibi mekanizmalarla belleği manuel olarak ayırmalı ve serbest bırakmalıdır. Bu, maksimum kontrol ve performans potansiyeli sunsa da, son derece hataya açık ve zaman alıcı bir süreçtir. Bellek sızıntıları ve use-after-free hataları, yönetilmeyen kodda karşılaşılan en yaygın ve hata ayıklaması zor sorunlardandır. İşte bu noktada C#, Java, Python gibi yönetilen (managed) diller ve bu dillerin çalıştığı platformlar (.NET, JVM gibi) devreye girer. Bu platformlar, Otomatik Bellek Yönetimi mekanizmaları sunar ve bu mekanizmaların en önemlisi Garbage Collection (GC — Çöp Toplama)’dır. GC, geliştiricinin yerine, uygulama tarafından artık kullanılmayan (yani “çöp” haline gelmiş) nesneleri otomatik olarak tespit eder ve bu nesnelerin kapladığı belleği geri kazanarak tekrar kullanılabilir hale getirir. .NET platformunda (ve dolayısıyla C#’ta) GC, Ortak Dil Çalışma Zamanı’nın (Common Language Runtime — CLR), modern .NET için CoreCLR’nin temel bir bileşenidir. Geliştiricilerin büyük çoğunluğunun bellek serbest bırakma işlemleriyle doğrudan uğraşmasına gerek kalmaz, bu da geliştirme sürecini önemli ölçüde hızlandırır ve birçok yaygın bellek hatasını önler. Ancak GC’nin nasıl çalıştığını anlamak, yüksek performanslı ve verimli .NET uygulamaları yazmak, olası performans darboğazlarını teşhis etmek ve yönetilmeyen kaynakları doğru bir şekilde yönetmek için hala önemlidir. Bu makalede, .NET’teki Garbage Collection mekanizmasının temellerini, nasıl çalıştığını (nesil tabanlı yaklaşım, mark-and-sweep algoritması), farklı GC modlarını, performans üzerindeki etkilerini, sonlandırıcıları (finalizers) ve IDisposable arayüzünü, bellek sızıntılarını nasıl önleyeceğimizi ve GC ile ilgili en iyi pratikleri derinlemesine inceleyeceğiz. Bölüm 1: Yönetilen Bellek (Managed Heap) ve Nesne Yaşam Döngüsü GC’nin nasıl çalıştığını anlamak için önce .NET’in belleği nasıl yönettiğine bakmamız gerekir. 1.1. Yönetilen Yığın (Managed Heap) Bir .NET uygulaması başlatıldığında, CLR işletim sisteminden sanal bellek adres alanı içinde belirli bir bölgeyi yönetimi altına alır. Bu bölgeye Yönetilen Yığın (Managed Heap) denir. Referans türündeki (sınıf örnekleri, diziler, string’ler, temsilciler vb.) tüm nesneler bu yığın üzerinde ayrılır. (Değer türleri — int, struct, bool vb. — genellikle doğrudan metodun çağrı yığınında (stack) veya içerdikleri referans türü nesnenin bir parçası olarak yığında bulunur, ancak “boxing” gibi durumlarda yığına da taşınabilirler). Yönetilen yığın, CLR tarafından verimli bir şekilde yönetilir. Yeni bir nesne oluşturulduğunda (new AnahtarKelimesi()), CLR yığında uygun boyutta boş bir alan bulur, belleği ayırır, nesnenin yapıcısını (constructor) çağırır ve nesneye bir referans (bellek adresi) döndürür. .NET’teki bellek ayırma işlemi genellikle çok hızlıdır, çünkü CLR bir sonraki boş bellek konumunu gösteren bir işaretçi tutar ve yeni nesneyi oraya yerleştirip işaretçiyi ilerletir (basitleştirilmiş bir açıklama). 1.2. Nesne Erişilebilirliği ve Kökler (Roots) GC’nin temel görevi, yığındaki hangi nesnelerin artık uygulama tarafından kullanılmadığını belirlemektir. Bir nesnenin “kullanımda” olup olmadığını anlamak için erişilebilirlik (reachability) kavramı kullanılır. Bir nesne, uygulamanın köklerinden (roots) başlayarak referans zincirleri aracılığıyla erişilebiliyorsa “canlı” (live) kabul edilir. Eğer bir nesneye hiçbir kökten ulaşılamıyorsa, o nesne “çöp” (garbage) haline gelmiştir ve belleği geri kazanılabilir. Kökler (Roots) Nelerdir? Uygulamanın doğrudan erişebildiği bellek konumlarıdır ve GC’nin erişilebilirlik analizine başladığı noktalardır: Global (Statik) Değişkenler: Statik alanlarda tutulan nesne referansları. Yerel Değişkenler ve Parametreler: Şu anda yürütülmekte olan metotların çağrı yığınlarındaki (stack) yerel değişkenler ve parametreler tarafından tutulan nesne referansları. CPU Yazmaçları (Registers): Şu anda CPU yazm

Apr 9, 2025 - 12:01
 0
C# ve .NET [24]

C# ve .NET’te Garbage Collection (Çöp Toplama): Otomatik Bellek Yönetiminin Derinlikleri

Giriş: Bellek Yönetiminin Zorluğu ve GC’nin Rolü

Yazılım geliştirmenin en temel zorluklarından biri bellek yönetimidir. Uygulamalar çalışırken, çeşitli nesneler (veri yapıları, sınıf örnekleri vb.) oluşturmak için belleğe ihtiyaç duyarlar. Bu belleğin verimli bir şekilde ayrılması (allocation) ve artık ihtiyaç duyulmadığında geri serbest bırakılması (deallocation) kritik öneme sahiptir. Eğer bellek düzgün bir şekilde serbest bırakılmazsa, uygulama zamanla giderek daha fazla bellek tüketir ve sonunda kaynakların tükenmesiyle veya performans sorunlarıyla karşılaşır. Bu duruma bellek sızıntısı (memory leak) denir. Diğer yandan, hala kullanımda olan bir bellek alanı yanlışlıkla serbest bırakılırsa, bu durum dangling pointer veya use-after-free hatalarına yol açarak uygulamanın çökmesine veya öngörülemeyen davranışlar sergilemesine neden olabilir.

C veya C++ gibi yönetilmeyen (unmanaged) dillerde, bellek yönetimi genellikle geliştiricinin sorumluluğundadır. Geliştiriciler malloc/free veya new/delete gibi mekanizmalarla belleği manuel olarak ayırmalı ve serbest bırakmalıdır. Bu, maksimum kontrol ve performans potansiyeli sunsa da, son derece hataya açık ve zaman alıcı bir süreçtir. Bellek sızıntıları ve use-after-free hataları, yönetilmeyen kodda karşılaşılan en yaygın ve hata ayıklaması zor sorunlardandır.

İşte bu noktada C#, Java, Python gibi yönetilen (managed) diller ve bu dillerin çalıştığı platformlar (.NET, JVM gibi) devreye girer. Bu platformlar, Otomatik Bellek Yönetimi mekanizmaları sunar ve bu mekanizmaların en önemlisi Garbage Collection (GC — Çöp Toplama)’dır. GC, geliştiricinin yerine, uygulama tarafından artık kullanılmayan (yani “çöp” haline gelmiş) nesneleri otomatik olarak tespit eder ve bu nesnelerin kapladığı belleği geri kazanarak tekrar kullanılabilir hale getirir.

.NET platformunda (ve dolayısıyla C#’ta) GC, Ortak Dil Çalışma Zamanı’nın (Common Language Runtime — CLR), modern .NET için CoreCLR’nin temel bir bileşenidir. Geliştiricilerin büyük çoğunluğunun bellek serbest bırakma işlemleriyle doğrudan uğraşmasına gerek kalmaz, bu da geliştirme sürecini önemli ölçüde hızlandırır ve birçok yaygın bellek hatasını önler. Ancak GC’nin nasıl çalıştığını anlamak, yüksek performanslı ve verimli .NET uygulamaları yazmak, olası performans darboğazlarını teşhis etmek ve yönetilmeyen kaynakları doğru bir şekilde yönetmek için hala önemlidir.

Bu makalede, .NET’teki Garbage Collection mekanizmasının temellerini, nasıl çalıştığını (nesil tabanlı yaklaşım, mark-and-sweep algoritması), farklı GC modlarını, performans üzerindeki etkilerini, sonlandırıcıları (finalizers) ve IDisposable arayüzünü, bellek sızıntılarını nasıl önleyeceğimizi ve GC ile ilgili en iyi pratikleri derinlemesine inceleyeceğiz.

Bölüm 1: Yönetilen Bellek (Managed Heap) ve Nesne Yaşam Döngüsü

GC’nin nasıl çalıştığını anlamak için önce .NET’in belleği nasıl yönettiğine bakmamız gerekir.

1.1. Yönetilen Yığın (Managed Heap)

Bir .NET uygulaması başlatıldığında, CLR işletim sisteminden sanal bellek adres alanı içinde belirli bir bölgeyi yönetimi altına alır. Bu bölgeye Yönetilen Yığın (Managed Heap) denir. Referans türündeki (sınıf örnekleri, diziler, string’ler, temsilciler vb.) tüm nesneler bu yığın üzerinde ayrılır. (Değer türleri — int, struct, bool vb. — genellikle doğrudan metodun çağrı yığınında (stack) veya içerdikleri referans türü nesnenin bir parçası olarak yığında bulunur, ancak “boxing” gibi durumlarda yığına da taşınabilirler).

Yönetilen yığın, CLR tarafından verimli bir şekilde yönetilir. Yeni bir nesne oluşturulduğunda (new AnahtarKelimesi()), CLR yığında uygun boyutta boş bir alan bulur, belleği ayırır, nesnenin yapıcısını (constructor) çağırır ve nesneye bir referans (bellek adresi) döndürür. .NET’teki bellek ayırma işlemi genellikle çok hızlıdır, çünkü CLR bir sonraki boş bellek konumunu gösteren bir işaretçi tutar ve yeni nesneyi oraya yerleştirip işaretçiyi ilerletir (basitleştirilmiş bir açıklama).

1.2. Nesne Erişilebilirliği ve Kökler (Roots)

GC’nin temel görevi, yığındaki hangi nesnelerin artık uygulama tarafından kullanılmadığını belirlemektir. Bir nesnenin “kullanımda” olup olmadığını anlamak için erişilebilirlik (reachability) kavramı kullanılır. Bir nesne, uygulamanın köklerinden (roots) başlayarak referans zincirleri aracılığıyla erişilebiliyorsa “canlı” (live) kabul edilir. Eğer bir nesneye hiçbir kökten ulaşılamıyorsa, o nesne “çöp” (garbage) haline gelmiştir ve belleği geri kazanılabilir.

Kökler (Roots) Nelerdir?

Uygulamanın doğrudan erişebildiği bellek konumlarıdır ve GC’nin erişilebilirlik analizine başladığı noktalardır:

Global (Statik) Değişkenler: Statik alanlarda tutulan nesne referansları.
Yerel Değişkenler ve Parametreler: Şu anda yürütülmekte olan metotların çağrı yığınlarındaki (stack) yerel değişkenler ve parametreler tarafından tutulan nesne referansları.
CPU Yazmaçları (Registers): Şu anda CPU yazmaçlarında bulunan nesne referansları (JIT derleyicisi optimizasyonları nedeniyle).
GC Tanıtıcıları (GC Handles): Yönetilen kodun yönetilmeyen kodla etkileşim kurarken belirli nesnelerin GC tarafından toplanmasını engellemek veya takip etmek için kullandığı özel yapılardır.
Sonlandırıcı Kuyruğu (Finalizer Queue): Sonlandırıcıya (finalizer) sahip olan ve henüz sonlandırıcısı çalıştırılmamış nesneler (aşağıda detaylandırılacak).
1.3. GC’nin Tetiklenmesi

GC otomatik olarak çalışır. Genellikle aşağıdaki durumlarda tetiklenir:

Bellek Ayırma İsteği Başarısız Olduğunda: Yönetilen yığında yeni bir nesne için yeterli boş alan bulunamadığında. Bu en yaygın tetiklenme nedenidir.
Sistem Belleği Düşük Olduğunda: İşletim sistemi düşük bellek durumu bildirdiğinde.
Periyodik Olarak (Ayarlara Bağlı): GC, belirli aralıklarla veya belirli bellek eşikleri aşıldığında proaktif olarak çalışabilir.
Manuel Olarak (GC.Collect()): Geliştirici GC.Collect() metodunu çağırarak GC’yi manuel olarak tetikleyebilir. Ancak bu genellikle kötü bir pratiktir ve performansı olumsuz etkileyebilir. GC’nin ne zaman çalışacağına karar vermesi genellikle daha verimlidir. Manuel çağırmanın gerekli olduğu çok nadir durumlar vardır (örneğin, belirli test senaryoları veya yönetilmeyen kaynaklarla ilgili özel durumlar).
Bölüm 2: .NET GC Algoritmaları ve Mekanizmaları

.NET GC’si, verimli ve performanslı olmak için birkaç karmaşık algoritma ve mekanizma kullanır.

2.1. İşaretle ve Süpür (Mark-and-Sweep) Algoritması (Temel Prensip)

Çoğu modern GC gibi, .NET GC’si de temel olarak bir “Mark-and-Sweep” varyantı kullanır:

İşaretleme (Mark) Fazı:

GC, tüm uygulama köklerini (roots) tarayarak başlar.
Köklerden doğrudan erişilebilen tüm nesneleri “canlı” olarak işaretler.
İşaretlenen nesnelerin referans verdiği diğer nesneleri takip eder ve onları da “canlı” olarak işaretler.
Bu işlem, köklerden erişilebilen tüm nesneler işaretlenene kadar tekrarlanır. Erişilebilen tüm nesnelerin bir grafiği oluşturulmuş olur.
Süpürme (Sweep) Fazı:

GC, yönetilen yığının tamamını tarar.
İşaretlenmemiş (yani köklerden erişilemeyen) tüm nesneleri “çöp” olarak kabul eder.
Bu çöp nesnelerin kapladığı bellek alanlarını serbest bırakır ve tekrar kullanılabilir hale getirir.
2.2. Sıkıştırma (Compacting) Fazı (İsteğe Bağlı)

Süpürme fazından sonra, yığında canlı nesneler arasında boşluklar (fragmentasyon) oluşabilir. Bu boşluklar, büyük nesneler için yeterli bitişik alan bulmayı zorlaştırabilir. Bu sorunu çözmek için GC, bir sıkıştırma (compacting) fazı gerçekleştirebilir:

Canlı nesneleri yığının bir ucuna doğru taşır, aradaki boşlukları ortadan kaldırır ve böylece büyük, bitişik bir boş alan oluşturur.
Nesneler taşındığı için, bu nesnelere işaret eden tüm referansların (köklerdeki ve diğer canlı nesnelerdeki) yeni bellek konumlarını gösterecek şekilde güncellenmesi gerekir. Bu, sıkıştırmanın en maliyetli kısmıdır.
Sıkıştırma her GC döngüsünde yapılmayabilir, çünkü maliyetlidir. GC, fragmentasyon seviyesine ve performans hedeflerine göre sıkıştırma yapıp yapmamaya karar verir.

2.3. Nesil Tabanlı Çöp Toplama (Generational Garbage Collection)

Tüm yığını her seferinde taramak (özellikle çok sayıda nesne varsa) çok maliyetli olabilir. .NET GC’si, performansı optimize etmek için Nesil Hipotezi (Generational Hypothesis) adı verilen bir gözleme dayanan Nesil Tabanlı GC yaklaşımını kullanır:

Hipotez 1: Yeni oluşturulan nesnelerin çoğu kısa ömürlüdür.
Hipotez 2: Eski nesnelerin çoğu uzun ömürlüdür.
Hipotez 3: Yığının tamamını taramak yerine sadece belirli bölgeleri taramak daha verimlidir.
Bu hipotezlere dayanarak, yönetilen yığın üç nesile (generation) ayrılır:

Nesil 0 (Generation 0 — Gen 0): En genç nesillerin bulunduğu bölgedir. Yeni oluşturulan tüm nesneler başlangıçta buraya yerleştirilir. Gen 0 koleksiyonları en sık gerçekleşir ve en hızlı olanlardır, çünkü sadece küçük bir bellek alanını ve genellikle kısa ömürlü nesneleri tararlar.
Nesil 1 (Generation 1 — Gen 1): Gen 0 koleksiyonundan sağ kurtulan (yani hala canlı olan) nesneler Gen 1'e terfi eder. Gen 1 koleksiyonları daha az sıklıkla gerçekleşir ve hem Gen 1'i hem de Gen 0'ı tarar.
Nesil 2 (Generation 2 — Gen 2): Gen 1 koleksiyonundan sağ kurtulan nesneler Gen 2'ye terfi eder. Burası en uzun ömürlü nesnelerin bulunduğu bölgedir. Gen 2 koleksiyonları en az sıklıkla gerçekleşir, ancak en maliyetli olanlardır çünkü tüm yönetilen yığını (Gen 0, Gen 1 ve Gen 2) tararlar ve genellikle sıkıştırma da bu aşamada yapılır.
Nesil Tabanlı GC Nasıl Çalışır?

Yeni nesneler Gen 0'a ayrılır.
Gen 0 dolduğunda, bir Gen 0 GC tetiklenir.
GC, Gen 0'daki canlı nesneleri işaretler.
Gen 0'daki çöp nesneler temizlenir.
Gen 0'daki canlı kalan nesneler Gen 1'e taşınır (terfi eder).
Gen 1 dolduğunda (veya belirli bir eşik aşıldığında), bir Gen 1 GC tetiklenir.
GC, Gen 1 ve Gen 0'daki canlı nesneleri işaretler (Gen 1'deki nesnelerin Gen 0'daki nesnelere referans verebileceğini unutmamak gerekir).
Gen 1 ve Gen 0'daki çöpler temizlenir.
Gen 1'den sağ kurtulanlar Gen 2'ye, Gen 0'dan sağ kurtulanlar Gen 1'e terfi eder.
Gen 2 dolduğunda (veya sistem belleği düşük olduğunda), bir Gen 2 GC (Full GC) tetiklenir.
GC, Gen 2, Gen 1 ve Gen 0'daki tüm canlı nesneleri işaretler.
Tüm yığındaki çöpler temizlenir.
Genellikle bu aşamada yığın sıkıştırılır.
Sağ kalan nesneler kendi nesillerinde kalır (Gen 2'den terfi edilecek başka bir yer yoktur).
Bu nesil tabanlı yaklaşım, GC’nin çoğu zaman sadece yığının küçük bir bölümüyle (Gen 0) ilgilenmesini sağlayarak performansı önemli ölçüde artırır.

2.4. Büyük Nesne Yığını (Large Object Heap — LOH)

Çok büyük nesnelerin (genellikle 85,000 bayttan büyük — bu eşik değişebilir) normal nesiller arasında sık sık kopyalanması (terfi ve sıkıştırma sırasında) çok maliyetli olurdu. Bu nedenle .NET, büyük nesneler için ayrı bir yığın bölgesi kullanır: Large Object Heap (LOH).

LOH’a ayrılan nesneler doğrudan Gen 2 nesneleri gibi kabul edilir.
LOH, varsayılan olarak sıkıştırılmaz. Bu, LOH üzerinde zamanla fragmentasyon oluşabileceği anlamına gelir. (Modern .NET sürümlerinde LOH sıkıştırması manuel olarak veya belirli koşullarda otomatik olarak tetiklenebilir, ancak dikkatli kullanılmalıdır).
Büyük nesneleri (özellikle dizileri) sık sık ayırmak ve serbest bırakmak LOH fragmentasyonuna ve performans sorunlarına yol açabilir. Bu nedenle, büyük nesnelerle çalışırken ArrayPool gibi havuzlama teknikleri kullanmak veya nesneleri yeniden kullanmak önemlidir.
Bölüm 3: GC Modları ve Yapılandırma

.NET GC’si farklı uygulama senaryolarına uyum sağlamak için farklı modlarda çalışabilir:

İş İstasyonu GC (Workstation GC): Varsayılan moddur. Tek işlemcili veya çok işlemcili istemci uygulamaları (masaüstü uygulamaları) için optimize edilmiştir. İki alt modu vardır:
Eşzamanlı (Concurrent) İş İstasyonu GC (Varsayılan): GC çalışırken uygulama iş parçacıklarının mümkün olduğunca kısa süre duraklatılmasını hedefler. Gen 2 koleksiyonları sırasında bile uygulama iş parçacıkları bir miktar çalışmaya devam edebilir (GC ile eşzamanlı). Bu, UI yanıt verirliği için önemlidir.
Eşzamansız (Non-concurrent) İş İstasyonu GC: GC çalışırken uygulama iş parçacıkları tamamen durdurulur. Daha az karmaşıktır ancak daha uzun duraklamalara neden olabilir.
Sunucu GC (Server GC): Çok işlemcili sunucu uygulamaları (ASP.NET Core vb.) için tasarlanmıştır. Yüksek verimlilik (throughput) ve ölçeklenebilirlik hedefler. Her mantıksal işlemci için ayrı bir GC yığını ve ayrı bir GC iş parçacığı oluşturur. Bu, GC işleminin paralelleştirilmesini sağlar ve daha fazla sayıda isteği daha hızlı işleyebilir. Ancak, tüm GC iş parçacıkları koordine olduğu için duraklama süreleri İş İstasyonu GC’ye göre biraz daha uzun olabilir, fakat toplam GC süresi genellikle daha kısadır.
Yapılandırma: GC modu genellikle proje dosyasında (.csproj) veya runtimeconfig.json dosyasında ayarlanır:


true

// runtimeconfig.json dosyasında
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true,
"System.GC.Concurrent": true // Sunucu GC için genellikle true'dur
}
}
}
Bölüm 4: Sonlandırıcılar (Finalizers) ve IDisposable

GC, yönetilen (managed) belleği otomatik olarak temizler. Ancak uygulamalar bazen yönetilmeyen (unmanaged) kaynakları da kullanabilir. Bunlar, GC’nin doğrudan yönetmediği kaynaklardır:

Dosya tanıtıcıları (File handles)
Ağ bağlantıları (Network connections)
Veritabanı bağlantıları
Grafik nesneleri (GDI handles vb.)
Doğrudan bellek ayırmaları (P/Invoke veya Marshal sınıfı ile)
Bu yönetilmeyen kaynakların işleri bittiğinde manuel olarak serbest bırakılması gerekir. Aksi takdirde kaynak sızıntıları oluşur. .NET bunun için iki ana mekanizma sunar: Sonlandırıcılar ve IDisposable arayüzü.

4.1. Sonlandırıcılar (Finalizers / Destructors)

Bir sınıfın, GC tarafından toplanmadan hemen önce çalıştırılacak özel bir metodu olabilir. C#’ta bu metot, sınıf adıyla aynı isme sahip, başında tilde (~) bulunan ve parametre almayan bir metot olarak tanımlanır (arka planda Object.Finalize() metodunu override eder).

public class UnmanagedResourceHolder
{
private IntPtr unmanagedHandle; // Yönetilmeyen kaynak tanıtıcısı (örnek)
private bool disposed = false;
public UnmanagedResourceHolder()
{
// Yönetilmeyen kaynağı al (örnek)
unmanagedHandle = NativeMethods.AllocateResource();
}
// Sonlandırıcı (C# destructor sözdizimi)
~UnmanagedResourceHolder()
{
// Yönetilmeyen kaynakları temizle
CleanUp(disposing: false);
Console.WriteLine("Finalizer çalıştı.");
}
// Yönetilmeyen kaynakları temizleyen yardımcı metot
protected virtual void CleanUp(bool disposing)
{
if (!disposed)
{
if (unmanagedHandle != IntPtr.Zero)
{
NativeMethods.ReleaseResource(unmanagedHandle); // Kaynağı serbest bırak
unmanagedHandle = IntPtr.Zero;
}
disposed = true;
}
}
// ... (IDisposable implementasyonu aşağıda eklenecek) ...
}
Sonlandırıcıların Çalışma Şekli ve Dezavantajları:

Ne Zaman Çalışır?: Bir nesnenin sonlandırıcısı varsa, GC bu nesneyi ilk koleksiyonda hemen toplamaz. Bunun yerine, nesneyi özel bir Sonlandırma Kuyruğuna (Finalization Queue) taşır. Ayrı bir yüksek öncelikli iş parçacığı bu kuyruktaki nesnelerin sonlandırıcılarını çalıştırır. Sonlandırıcı çalıştıktan sonra, bir sonraki GC döngüsünde nesne gerçekten toplanabilir.
Garanti Yok: Sonlandırıcının ne zaman çalışacağı veya çalışıp çalışmayacağı (örneğin, uygulama aniden sonlanırsa) garanti edilmez.
Performans Maliyeti: Sonlandırıcıya sahip nesnelerin toplanması en az iki GC döngüsü sürer (birinde kuyruğa alınır, diğerinde toplanır). Bu, nesnenin bellekte daha uzun süre kalmasına neden olur. Sonlandırma kuyruğunu işleyen thread de ek bir yük getirir.
Sıra Yok: Sonlandırıcıların hangi sırada çalışacağı garanti edilmez.
Kullanım: Sonlandırıcılar, yalnızca sınıfın doğrudan sahip olduğu yönetilmeyen kaynakları serbest bırakmak için son çare olarak kullanılmalıdır. Eğer sınıf sadece yönetilen nesnelere (diğer IDisposable nesneler dahil) referans veriyorsa, sonlandırıcıya ihtiyaç yoktur ve eklenmemelidir.
4.2. IDisposable Arayüzü ve Dispose Deseni

Yönetilmeyen kaynakları deterministik (öngörülebilir) bir şekilde serbest bırakmanın standart ve önerilen yolu System.IDisposable arayüzünü uygulamaktır. Bu arayüz, tek bir metot tanımlar: Dispose().

Dispose Deseni:

IDisposable uygulayan sınıflar genellikle standart bir desen takip eder:

public class ManagedAndUnmanagedResourceHolder : IDisposable
{
private SafeHandle resourceHandle = new SafeFileHandle(IntPtr.Zero, true); // Yönetilmeyen kaynak için SafeHandle kullanmak daha iyi
private Component managedComponent = new Component(); // Yönetilen IDisposable kaynak
private bool disposed = false; // Dispose'un tekrar çağrılmasını engellemek için
// Public Dispose metodu (IDisposable arayüzünden gelir)
public void Dispose()
{
// Deterministik temizlik yap
Dispose(disposing: true);
// Sonlandırıcının tekrar çağrılmasını engelle (çünkü kaynaklar zaten temizlendi)
GC.SuppressFinalize(this);
}
// Protected virtual Dispose metodu (asıl temizlik işini yapar)
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return; // Zaten Dispose edilmişse bir şey yapma
}
if (disposing)
{
// Yönetilen kaynakları temizle (sadece Dispose çağrıldığında)
if (managedComponent != null)
{
managedComponent.Dispose();
managedComponent = null;
}
// Diğer yönetilen nesneler burada null'a çekilebilir (opsiyonel)
}
// Yönetilmeyen kaynakları temizle (hem Dispose hem de Finalizer tarafından çağrılır)
if (resourceHandle != null && !resourceHandle.IsInvalid)
{
resourceHandle.Dispose(); // SafeHandle'ın Dispose'u yönetilmeyen kaynağı serbest bırakır
}
disposed = true;
}
// Sonlandırıcı (SADECE yönetilmeyen kaynak varsa gereklidir!)
// Eğer sınıf sadece yönetilen kaynaklar içeriyorsa bu metot OLMAMALIDIR.
~ManagedAndUnmanagedResourceHolder()
{
// Yönetilmeyen kaynakları temizle (yönetilenleri değil!)
Dispose(disposing: false);
// Console.WriteLine("Finalizer çalıştı.");
}
// Sınıfın diğer metotları...
public void DoWork()
{
if (disposed)
{
throw new ObjectDisposedException(nameof(ManagedAndUnmanagedResourceHolder));
}
// ... iş mantığı ...
}
}
Temel Noktalar:

Dispose() public metodu, kullanıcı tarafından çağrılacak olan metottur.
Dispose(bool disposing) protected virtual metodu asıl temizlik mantığını içerir. disposing parametresi, metodun Dispose() tarafından mı (true) yoksa sonlandırıcı tarafından mı (false) çağrıldığını belirtir.
disposing == true ise, hem yönetilen hem de yönetilmeyen kaynaklar temizlenir.
disposing == false (yani sonlandırıcıdan çağrıldığında), sadece yönetilmeyen kaynaklar temizlenir. Yönetilen nesnelere dokunulmaz, çünkü onların durumu belirsiz olabilir (GC tarafından toplanmış olabilirler).
GC.SuppressFinalize(this): Dispose() çağrıldığında, kaynaklar zaten temizlendiği için sonlandırıcının tekrar çalışmasına gerek yoktur. Bu metot, nesneyi sonlandırma kuyruğundan çıkararak gereksiz çalışmayı ve performans kaybını önler.
SafeHandle Kullanımı: Yönetilmeyen kaynak tanıtıcılarını (IntPtr) doğrudan tutmak yerine, Microsoft.Win32.SafeHandles isim alanındaki SafeHandle türevlerini (örn. SafeFileHandle) kullanmak genellikle daha güvenlidir. SafeHandle, tanıtıcının düzgün bir şekilde serbest bırakılmasını ve bazı güvenlik sorunlarını (handle recycling) önlemeye yardımcı olur. SafeHandle kendisi IDisposable uygular ve sonlandırıcı içerir.
4.3. using İfadesi: Deterministik Temizlik

IDisposable uygulayan nesnelerin Dispose() metodunun deterministik olarak çağrılmasını garanti etmenin en iyi yolu using ifadesidir:

// using ifadesi, kapsam dışına çıkıldığında otomatik olarak holder.Dispose() çağırır.
using (var holder = new ManagedAndUnmanagedResourceHolder())
{
holder.DoWork();
} // holder.Dispose() burada çağrılır (hata olsa bile)
// C# 8.0+ using bildirimi:
using var anotherHolder = new ManagedAndUnmanagedResourceHolder();
anotherHolder.DoWork();
// anotherHolder, mevcut kapsamın (metot veya blok) sonuna ulaşıldığında Dispose edilir.
Özetle: Yönetilmeyen kaynaklarla çalışıyorsanız IDisposable arayüzünü uygulayın. Sadece yönetilmeyen kaynağınız varsa sonlandırıcı ekleyin (ve SafeHandle kullanmayı düşünün). IDisposable nesnelerini her zaman using ifadesi ile kullanın.

Bölüm 5: GC Performansı ve Bellek Sızıntıları

GC otomatik olsa da, uygulamanızın performansını etkileyebilir.

5.1. GC Duraklamaları (GC Pauses)

GC çalıştığında (özellikle Gen 2 ve LOH koleksiyonları sırasında ve sıkıştırma yaparken), uygulama iş parçacıklarının kısa bir süre için duraklatılması gerekebilir. Bu duraklamalar, özellikle gerçek zamanlı sistemlerde veya düşük gecikme gerektiren uygulamalarda (oyunlar, finansal uygulamalar) fark edilebilir olabilir.

Duraklamaları Azaltma İpuçları:

Gereksiz Bellek Ayırmalarından Kaçının: Özellikle sık çalışan döngüler içinde veya sık çağrılan metotlarda nesne oluşturmaktan kaçının. Nesneleri yeniden kullanın (pooling), değer türlerini (struct) uygun yerlerde kullanın, StringBuilder ile string birleştirmesi yapın.
Nesil 0 Koleksiyonlarını Teşvik Edin: Kısa ömürlü nesneler oluşturmak genellikle daha verimlidir, çünkü Gen 0 koleksiyonları hızlıdır. Uzun ömürlü nesnelerin sayısını minimize etmeye çalışın.
LOH Ayırmalarını Azaltın: Büyük diziler veya nesnelerle çalışırken ArrayPool gibi havuzlama mekanizmalarını kullanın.
Sunucu GC Modunu Kullanın: Sunucu uygulamaları için Sunucu GC genellikle daha iyi verimlilik sağlar.
GC.TryStartNoGCRegion / EndNoGCRegion: Performansın çok kritik olduğu kısa kod bölümlerinde GC’yi geçici olarak engellemek için kullanılabilir (çok dikkatli kullanılmalıdır!).
Profilleme Araçları Kullanın: Visual Studio Diagnostic Tools, PerfView, dotMemory gibi araçlar kullanarak bellek ayırmalarını, GC davranışlarını ve olası sorunları analiz edin.
5.2. Yönetilen Bellek Sızıntıları

GC otomatik olsa da, “yönetilen bellek sızıntıları” hala mümkündür. Bu durum, artık mantıksal olarak ihtiyaç duyulmayan nesnelerin hala canlı kökler tarafından referans alınması nedeniyle GC tarafından toplanamamasıdır.

Yaygın Nedenler:

Statik Alanlar: Statik alanlarda tutulan nesneler uygulama ömrü boyunca yaşar. Bir listeye veya dictionary’ye statik olarak sürekli nesne ekleyip hiç çıkarmamak sızıntıya yol açar.
Olay Abonelikleri (Event Handlers): Uzun ömürlü bir nesne (yayıncı), kısa ömürlü bir nesnenin (abone) olayına abone olursa ve abonelikten çıkılmazsa (-=), yayıncı aboneye bir referans tutmaya devam eder ve abonenin GC tarafından toplanmasını engeller. Bu en yaygın sızıntı nedenlerinden biridir. Çözüm: Abone artık gerekli olmadığında mutlaka abonelikten çıkın (Dispose içinde veya başka uygun bir zamanda). Weak Event Pattern de bir alternatiftir.
Önbellekler (Caches): Önbelleğe alınan nesneler, önbellekten manuel olarak çıkarılmadıkça veya bir süre sonu (expiration) mekanizması olmadıkça bellekte kalmaya devam eder.
Closure’lar (Kapanımlar): Lambda ifadeleri veya anonim metotlar tarafından yakalanan değişkenler, lambda’nın kendisi yaşadığı sürece yaşar. Eğer lambda uzun ömürlü bir nesneye (örn. statik olay yöneticisi) atanırsa, yakalanan nesneler de uzun ömürlü hale gelir.
Bellek sızıntılarını bulmak için profilleyici araçlar kullanmak genellikle en etkili yoldur.

Bölüm 6: Sonuç ve En İyi Pratikler Özeti

.NET Garbage Collector, geliştiriciler için büyük bir kolaylık sağlayan karmaşık ve sofistike bir mekanizmadır. Otomatik bellek yönetimi sayesinde geliştiriciler, düşük seviyeli bellek detayları yerine iş mantığına odaklanabilirler. Ancak GC’nin nasıl çalıştığını anlamak, yüksek performanslı uygulamalar yazmak ve olası sorunları gidermek için önemlidir.

En İyi Pratikler Özeti:

GC’ye Güvenin: GC.Collect() metodunu manuel olarak çağırmaktan kaçının. GC genellikle ne zaman çalışacağını daha iyi bilir.
Gereksiz Ayırmaları Azaltın: Özellikle sık çalışan kod yollarında nesne oluşturmayı minimize edin. Nesne havuzlama, StringBuilder, Span, Memory gibi teknikleri değerlendirin.
LOH Kullanımına Dikkat Edin: Büyük nesnelerle çalışırken ArrayPool kullanın.
IDisposable’ı Doğru Kullanın: Yönetilmeyen kaynaklar veya yönetilen IDisposable nesneler içeren sınıflarda IDisposable arayüzünü uygulayın ve Dispose desenini doğru bir şekilde kullanın.
using İfadesini Kullanın: IDisposable nesnelerinin deterministik olarak temizlenmesini garanti altına almak için her zaman using ifadesini (veya await using) tercih edin.
Sonlandırıcılardan Kaçının (Gerekmedikçe): Sonlandırıcıları yalnızca sınıfınız doğrudan yönetilmeyen kaynaklara sahipse ve Dispose’un çağrılmama ihtimaline karşı bir geri dönüş mekanizması olarak ekleyin. Performans maliyetini unutmayın. SafeHandle kullanmayı düşünün.
Bellek Sızıntılarına Dikkat Edin: Olay aboneliklerinden çıkmayı unutmayın, statik referansları dikkatli kullanın, önbellekleri yönetin.
Doğru GC Modunu Seçin: Uygulama türünüze (İş İstasyonu veya Sunucu) uygun GC modunu yapılandırın.
Profilleyici Kullanın: Bellek kullanımı ve GC davranışını anlamak ve sorunları teşhis etmek için Visual Studio Diagnostic Tools, PerfView, dotMemory gibi araçları kullanmaktan çekinmeyin.
Garbage Collection, .NET platformunun temel bir gücüdür. Nasıl çalıştığını anlayarak ve en iyi pratikleri uygulayarak, GC’nin avantajlarından tam olarak yararlanabilir ve daha sağlam, performanslı ve verimli C# uygulamaları geliştirebilirsiniz.

Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Kişisel WebSite
Abdulkadir Güngör - Özgeçmiş
Github
Github
Linkedin