Merhaba,
Bellek yönetimi ve Garbage Collection genelde temel bir konu gibi görünür. Nesnelerin yaşam sürelerine göre yönetilmesi ve farklı nesil yapıları çoğu zaman öğrenilir ama pratikte işler biraz daha farklıdır.
Özellikle performans sorunları olduğunda önemli soru şudur: Garbage Collector (GC) ne zaman çalışır ve neden çalışır?
Bu konu, yoğun çalışan sistemlerde veya beklenmeyen yavaşlamalarda daha da önem kazanır. Çünkü GC sadece arka planda çalışan basit bir mekanizma değildir; doğru koşullarda sistem performansını doğrudan etkiler. Bu yazıda teoriden çok pratik tarafa odaklanacağız. GC’nin hangi durumlarda nasıl davrandığını ve hangi işaretlerin problem olabileceğini inceleyeceğiz.
Garbage Collector ve Nesil Mantığı
.NET Garbage Collector, kullanılmayan nesnelerin bellekte otomatik olarak temizlenmesini sağlayarak geliştiricilerin yükünü büyük ölçüde hafifletir. Bu mekanizma sayesinde çoğu durumda Dispose, using veya manuel bellek serbest bırakma işlemleriyle doğrudan uğraşmanıza gerek kalmaz. Ancak yönetilmeyen kaynaklar olan dosya handle’ları, ağ bağlantıları veya veritabanı bağlantıları söz konusu olduğunda, IDisposable arayüzü ve using blokları kaynakların zamanında serbest bırakılması için hâlâ kritik bir rol oynamaya devam eder.
GC, verimliliği artırmak için nesneleri yaşam sürelerine göre farklı kategorilere ayırır. Bu sistemin temelindeki varsayım şudur: Çoğu nesne oluşturulduktan kısa bir süre sonra kullanılmaz hale gelir. Buradan yola çıkarak bellek, yönetim kolaylığı ve performans için üç farklı nesle bölünmüştür:
-
Gen 0: Yeni oluşturulan tüm nesnelerin ilk durağı burasıdır. Bu alandaki nesneler genellikle geçici işler için kullanılır ve ilk temizlik döngüsünde bellekten silinmeleri beklenir.
-
Gen 1: Gen 0 temizliğinden sağ çıkan ancak hâlâ kısa ömürlü olabileceği düşünülen nesneleri barındırır.
-
Gen 2: Uzun süre yaşayan ve genellikle uygulamanın yaşam döngüsü boyunca bellekte kalacak olan nesneler (örneğin statik veriler veya cache mekanizmaları) burada toplanır.
Bu katmanlı yapı sayesinde Garbage Collector, her seferinde tüm belleği taramak yerine, en sık boşalan ve erişilemez duruma düşen Gen 0 nesnelerine odaklanır. Bu strateji, sistem kaynaklarının çok daha verimli kullanılmasını sağlar ve uygulama üzerindeki performans yükünü minimize eder.
GC Ne Zaman Çalışır?
Garbage Collector'ın çalışmasını tetikleyen birkaç temel durum vardır. En yaygın senaryo, yeni bir nesne oluşturulurken heap üzerinde bu nesneye yetecek kadar yer kalmamasıdır. Heap, uygulamanın runtime sırasında nesneleri barındırdığı bellek alanıdır. Bu alanda yer kalmadığında GC otomatik olarak devreye girer ve bellekte boş yer açmaya çalışır.
Bunun yanı sıra, fiziksel bellek üzerindeki baskılar da GC'yi tetikleyebilir. Eğer sistem genelinde fiziksel bellek azalıyorsa veya işletim sistemi uygulamanızdan bellek boşaltmasını talep ediyorsa, Garbage Collector sistem sağlığını korumak adına çalışmaya başlar.
Bir diğer tetikleyici ise geliştirici tarafından manuel olarak yapılan GC.Collect() çağrısıdır. Ancak bu yaklaşım genellikle önerilmez; çünkü GC, kendi algoritmasına göre en uygun zamanda çalışacak şekilde optimize edilmiştir. Manuel müdahaleler, çoğu durumda gereksiz yere tüm nesil seviyelerini kapsayan kapsamlı bir temizlik döngüsünü tetikleyerek performans düşüşlerine ve uygulamada kısa süreli duraksamalara neden olabilir.
Garbage Collector bir temizlik kararı aldığında, süreci her zaman optimize etmek adına öncelikle düşük nesilleri (Gen 0 ve Gen 1) kontrol etmeyi hedefler. Bu sayede en az maliyetle en fazla alanı boşaltmaya çalışır.
Gen 0 Ne Zaman Çalışır?
Gen 0, Garbage Collector'ın en sık ziyaret ettiği ve en hızlı temizlik yaptığı bölgedir. .NET ekosisteminde uygulamalar sürekli yeni nesneler üretir; bu işleme allocation (yer ayırma) denir. Gen 0, bu yeni doğan nesnelerin ilk durağıdır.
Ancak bu alanın kapasitesi sınırlıdır. Gen 0 alanı dolduğunda, çalışma zamanı bir temizlik döngüsü başlatır. Bu aşamada GC, kimlerin işi bitti, kimler hala lazım? diye kontrol eder. Herhangi bir referansı kalmamış nesneler anında bellekten silinir. Hala bir değişkene veya işleme bağlı olan nesneler, kısa ömürlü değilmiş kabul edilerek bir üst seviye olan Gen 1'e taşınır.
for (int i = 0; i < 100000; i++)
{
var temp = new object(); // Döngü her döndüğünde eski 'temp' boşa düşer.
}
Bu kod blokunda nesneler oluşturulur ve saniyeler içinde işlevini yitirir. Bu durum Gen 0'ın sürekli dolmasına ve GC'nin çok sık ancak çok maliyetli olmayan kısa süreli temizlikler yapmasına neden olur.
Gen 1 Ne Zaman Çalışır?
Gen 1, kısa ömürlü nesneler ile uzun ömürlü nesneler arasındaki tampon bölgedir. Bir nesnenin buraya gelmesi, onun en az bir Gen 0 temizliğinden sağ çıktığı anlamına gelir.
Gen 0 temizliği bittiğinde hala bir referans tarafından tutulan nesneler silinmez ve Gen 1 alanına aktarılır. GC, Gen 0'ı her temizlediğinde mutlaka Gen 1'in doluluk oranını da kontrol eder. Eğer Gen 0 temizliği sistemin bellek ihtiyacını karşılamaya yetmiyorsa, GC kapsamını genişleterek Gen 1'i de temizlemeye başlar.
List<object> buffers = new();
for (int i = 0; i < 100000; i++)
{
buffers.Add(new object()); // Nesneler listeye eklendiği için referansları kopmaz.
}
Burada nesneler temp değişkenindeki gibi hemen yok olmaz; bir koleksiyon (List) içinde tutuldukları için "erişilebilir" kalırlar. Bu yüzden ilk GC döngüsünde silinmek yerine aday olarak Gen 1 seviyesine taşınırlar.
Gen 2 Ne Zaman Çalışır?
Gen 2, .NET bellek yönetiminin en ağır ve en maliyetli seviyesidir. Buradaki nesneler artık kalıcı veya çok uzun ömürlü olarak etiketlenmiştir. Gen 2'nin temizlenmesi demek, tüm heap'in taranması demektir ki bu durum genellikle uygulama performansında hissedilir duraksamalara yol açar.
Gen 2 temizliği şu kritik anlarda tetiklenir:
-
Genel Bellek Baskısı: Heap alanı genel olarak dolmaya başladığında.
-
Yetersiz Alan: Gen 0 ve Gen 1 temizlikleri bittiği halde hala yeterli bellek alanı açılamadığında.
-
Kritik Seviye: Uzun ömürlü nesnelerin kapladığı alan sistem eşik değerlerini aştığında.
static List<object> cache = new();
for (int i = 0; i < 10000; i++)
{
cache.Add(new object());
}
Önceki örneklerden farklı olarak burada static bir yapı kullanılmıştır. Statik nesneler uygulama kapanana kadar referanslarını korudukları için temizlenemezler. Her temizlik döngüsünde (Gen 0 -> Gen 1 -> Gen 2) hayatta kalarak adım adım Gen 2 seviyesine kadar terfi ederler. Bu da Gen 2'nin şişmesine ve sistemin daha ağır çalışmasına neden olabilir.
GC Akışı
Garbage Collection süreci, tamamen hiyerarşik bir mantıkla ve en ucuz maliyetten en pahalıya doğru ilerleyen adım adım bir yapıya sahiptir. Her aşama bir önceki aşamanın sonucuna göre tetiklenir. Süreç, uygulama içinde yeni bir nesne oluşturulduğunda başlar; bu nesne heap üzerindeki en dinamik ve hızlı temizlenen bölge olan Gen 0 alanına yerleştirilir. GC, bellek trafiğinin en yoğun olduğu bu bölgede en sık taramasını gerçekleştirir.
Zamanla yeni nesne oluşturma (allocation) işlemleriyle Gen 0 alanı dolar veya belirli bir eşik aşılır. Bu durumda Garbage Collector devreye girerek ilk müdahalesini Gen 0 üzerinde yapar. Buradaki temel amaç, artık bir referansı kalmayan ve erişilemez duruma düşen nesneleri tespit edip bellekten hızla kaldırmaktır. Eğer bu temizlik sonucunda yeterli alan açılamazsa, yani Gen 0’dan sağ çıkan nesne sayısı fazlaysa, bu nesneler terfi ederek bir üst seviye olan Gen 1’e taşınır.
Süreç Gen 1 seviyesine geçtiğinde, GC buradaki nesneleri de kontrol ederek erişilemez olanları temizler. Ancak sistemdeki bellek baskısı hala devam ediyorsa ve Gen 1 temizliği de ihtiyacı karşılamıyorsa, en maliyetli aşama olan Gen 2 seviyesine geçilir. Gen 2, uzun ömürlü nesnelerin bulunduğu alandır ve burada yapılan temizlik tüm heap alanını kapsadığı için çok daha kapsamlıdır. Bu aşamada sadece kısa süreli nesneler değil, uygulamanın yaşam döngüsü boyunca orada olan tüm nesneler değerlendirilir.
Özetle, Gen 2 seviyesinde yapılan GC işlemleri oldukça ağırdır ve uygulamada Stop-the-world olarak bilinen kısa süreli duraksamalara neden olabilir. Bu nedenle tüm süreç, her zaman en düşük maliyetli seviyeden Gen 0 başlar ve sadece ihtiyaç duyulduğunda yukarı doğru genişler. Garbage Collector'ın temel felsefesi, sisteme büyük yük getirecek pahalı işlemleri yapmadan önce her zaman daha ucuz ve hızlı temizleme yöntemlerini tüketmektir.
GC Nasıl Takip Edilir
NET içinde GC davranışını gözlemlemek için kullanılan temel araçlardan biri dotnet-counters aracıdır. Bu araç, çalışan bir uygulamanın runtime metriklerini canlı olarak izlemeyi sağlar. Öncelikle bu tool’un sistemde kurulu olması gerekir. Kurulu değilse global olarak şu komut ile yüklenir:
dotnet tool install --global dotnet-counters
Kurulumdan sonra bir sonraki adım, izlenecek uygulamanın Process ID (PID) değerini bulmaktır. PID, işletim sistemi tarafından çalışan her uygulamaya verilen benzersiz kimliktir ve hangi process’in izleneceğini belirler. İzlemek istenen .NET uygulamasının çalışıyor olması gerekir ve bu uygulamanın PID bilgisi alınmalıdır.
Çalışan .NET uygulamalarının PID listesini görmek için şu komut kullanılır:
dotnet-counters ps
Bu komut, sistemde çalışan .NET process’lerini listeler ve her biri için PID değerini gösterir. Çıktıda uygulamanın adının (.exe uzantılı dosya adı) yer aldığı satırın en solunda bulunan sayı PID’dir. İzlemek istediğimiz uygulamayı bu isimden ayırt edip, sol taraftaki PID değerini kullanarak hedef process’i seçeriz.
PID alındıktan sonra GC metriklerini izlemek için komut çalıştırılır. Örneğin PID değeri 8872 ise:
dotnet-counters monitor -p 8872 System.Runtime
Komut satırındaki -p parametresi hedef uygulamanın kimliğini belirtirken, System.Runtime ifadesi .NET runtime metrik grubunu temsil eder. Komut çalıştıktan sonra ekranda gerçek zamanlı veriler akmaya başlayacaktır. Bellek yönetimi performansını analiz etmek için şu üç metrik hayati önem taşır:
-
Gen 0 GC Count: En sık gerçekleşen hızlı temizliklerin sayısı.
-
Gen 1 GC Count: Orta ömürlü nesnelerin temizlenme sıklığı.
-
Gen 2 GC Count: En maliyetli olan Full GC operasyonlarının sayısı.
Bu değerleri takip ederek uygulamanızın hangi nesil seviyesinde ne kadar yük altında olduğunu görebilir ve olası bellek sızıntılarını veya yanlış nesne yönetim stratejilerini tespit edebilirsiniz.
Görseldeki Verileri Nasıl Okumalıyız?
Yazı boyunca ele aldığımız teorik sürecin uygulama tarafındaki karşılığını analiz ettiğimizde, karşımıza çıkan tablo uygulamanın bellek yönetim karakterini net bir şekilde ortaya koyar. Görseldeki verileri teknik açıdan inceleyelim:
Tablodaki değerler, uygulamanın yaşam döngüsü boyunca her nesil için GC’nin kaç kez tetiklendiğini gösterir. Görselde dikkat çeken en kritik detay, nesil sayılarının birbirine çok yakın olmasıdır. Normal şartlarda Gen 0 temizlik sayısının çok daha yüksek, Gen 1 ve Gen 2 sayılarının ise çok daha düşük olması beklenir. Bu sayıların birbirine bu kadar yakın seyretmesi, nesnelerin Gen 0'da temizlenemeden hızla üst nesillere aktarıldığını ve bellekte kısa sürede baskı oluşturduğunu gösterir.
Sağlıklı bir .NET uygulamasında Gen 0 > Gen 1 > Gen 2 şeklinde belirgin bir hiyerarşi beklenir. Temel prensip, nesnelerin büyük çoğunluğunun Gen 0’da temizlenmesi ve çok az bir kısmının Gen 2’ye kadar hayatta kalmasıdır.
Görselde Gen 2 sayısının 10, Gen 0 sayısına 13 bu denli yakın olması, kısa ömürlü olması beklenen nesnelerin bir şekilde üst nesillere taşındığını veya yoğun bir trafik olduğunu gösterir. Gen 2 temizliğinin 10 kez yapılması, sistemin 10 kez en yüksek maliyetli Full GC operasyonunu gerçekleştirdiğini kanıtlar. Eğer bu metrik kısa zaman dilimlerinde hızla yükseliyorsa, uygulamada ciddi performans darboğazları ve duraksamalar kaçınılmazdır.
dotnet.assembly.count değerinin 180 olması, runtime sırasında belleğe yüklenen toplam kütüphane sayısını ifade eder. Bu değerin uygulama ayağa kalktıktan sonra stabilize olması gerekir; sürekli bir artış gözlemleniyorsa dinamik assembly yükleme süreçlerinde bir bellek sızıntısı ihtimali değerlendirilmelidir.
Memory Leak (Bellek Sızıntısı)
Garbage Collector sadece ulaşılamayan nesneleri silebilir. Eğer bir nesneye hâlâ aktif bir referans varsa (örneğin statik bir listeye eklenip unutulmuşsa), GC bu nesneyi hâlâ lazım zanneder ve silemez. Bu durumda nesneler her temizlik döngüsünde hayatta kalarak sürekli Gen 2 seviyesine terfi eder ve orada birikerek uygulamanın RAM kullanımını kontrolsüzce artırır. Eğer dotnet-counters verilerinde uygulama stabilize olduktan sonra bile Gen 2 sayısının ve toplam bellek kullanımının sürekli yukarı yönlü bir ivme kazandığını görüyorsanız, içeride temizlenemeyen bir bellek sızıntısı var demektir.
Sonuç
Garbage Collector’ı sadece arka planda kendi kendine çalışan basit bir temizlik sistemi olarak görmek yanlış olur. Modern ve yüksek performanslı .NET uygulamaları geliştirmek için bu mekanizmanın nasıl nefes aldığını bilmek gerekir. Uygulamanızın bellek sağlığını analiz ederken asıl odaklanmanız gereken üç temel soru şudur: Hangi nesil çalışıyor, ne sıklıkla çalışıyor ve bu çalışmayı ne tetikliyor?
Gen 0 döngüleri genellikle uygulamanın doğal işleyişini gösteren sağlıklı işaretlerdir. Ancak Gen 2 sayısının hızla artması, çoğu zaman ciddi bir performans probleminin ve sistem üzerindeki ağır bellek baskısının habercisidir. Eğer uygulamanızda sebebi net olmayan yavaşlamalar veya anlık donmalar yaşıyorsanız, bakmanız gereken ilk yer her zaman GC metrikleri olmalıdır. Doğru analiz edilen veriler, gizli darboğazları ortaya çıkararak sisteminizi çok daha stabil bir hale getirmenize olanak tanır.
Diğer bloglarımda görüşmek üzere 👋