Garbage Collection Nedir? Nasıl Çalışır?
Garbage collection bize CLR’ın sunduğu özelliklerden biridir. Low level dillerin aksine high level diller bize memory operasyonlarıyla uğraşmamamızı ve bunların büyük kısmını kendisinin yapacağını vaadeder. Garbage collection da bunlardan biri. Peki Garbage collection tam olarak ne iş yapar? Garbage collection belli aralıklarla heapte bulunan dereferenced objeleri temizler.
Konunun detayları için .Net Memory Management yazımı okumanızı tavsiye ederim
Önce memory’le ilgili bilinmesi gereken bazı kavramları açıklayarak başlayalım.
Memory Leak : Bellekte ayrılmış fakat serbest bırakılması unutulmuş alan
Memory Corruption : Silinen objeyi kullanma çabası
Memory Error : Allocate edilmemiş objeyi silme çabası
Garbage collection bu gibi hataların önüne geçer.
MARK & SWEEP & COMPACT AŞAMALARI
Garbage collector her çalıştığında 3 ana step gerçekleştirir. Mark, Sweep ve Compact.
Mark Stage : Heapte bulunan ve dereferenced olmayan tüm objelerin tespit edildiği aşamadır.
Sweep Stage : Bu tespit edilen objelerden deferenced olanların silindiği aşama. Bu aşama dereferenced objeler silindikten sonra heapte bir boşluk yaratır.
Compact Stage : Silinmeden sonra bellekte oluşan boşlukların temizlendiği step.
GENERATIONAL HEAP
Mark-Sweep-Compact 3’lüsü bir arada oldukça yavaş çalışmakta ve Garbage Collector live objeleri acaba dereferenced oldular mı diye gerekli gereksiz check etmek durumdan kalmaktadır. Bu noktada heapin generational yapısı devreye girmektedir. Heap gen0, gen1, gen2 olarak adlandırılan 3 generation’ dan oluşmaktadır. Generationlar aslında heapin mantıksal olarak bölümlere ayrılmış halidir. Yarattıkları fark ise, bu heap generationlarının garbage collector tarafından taranma sıklığıdır.
Gen0 : Çok sık kontrol edilir. Bir object ilk kontrolden sonra hala deferenced olmamışsa yani yaşamını sürdürüyorsa Gen1’e alınır.
Gen1 : Gen0’a göre daha az kontrol edilir. Bir object Gen1’in kontrolünden sonra hala dereferenced olmadıysa Gen2’ye taşınır.
Gen2 : Çok az sıklıkla kontrol edilir.
Buradaki ana mantık bir obje belli taramalardan sonra hala yaşayan haldeyse long-live bir objedir bu sebeple daha az kontrol edilmelidir varsayımıdır. Bir array gen1’e geçmiş iken ona gelen yeni bir eleman gen0’da olabilir. Hatta aynı array gen2 de olup gösterdiği eleman gen0’da dahi olabilir.
SMALL OBJECT HEAP VS LARGE OBJECT HEAP
Yukarıda da bahsettiğim gibi generational heap mantığına göre sürekli bir sonraki Gen’e kayan objeler long live objelerdir ve sürekli sıklıkla kontrol edilmeye ihtiyaçları yoktur. Sonuç olarak, gen0’ın limiti kısıtlıdır ve bu sayede boşaltılmış olur ve gen2ye kadar giden objeler çok fazla check edilmemiş olur. Ancak boyutu çok büyük objeleri de Gen0’dan Gen1’e, Gen1’den de Gen2ye taşımak performans kaybı yaratır. Bu sebeple heap kendi arasında small object heap ve large object heap olarak ayrılmıştır.
Obje 85 kilobytetan küçük ise Small object heap’e (SOH) ki bu yukarıda da bahsettiğimiz 3 heapli olan – mark sweep compact – ‘dır.
Obje 85 kilobytetan büyük ise Large object heap’e (LOH) alınır. LOH generasyon SOH gibi generationlara sahip değildir ama gen2ye senkron olmuş vaziyettedir.
Bu sayede büyük boyutlu objeler gen0’dan 3-4 operasyon ile gen2ye kadar geçirilmek yerine direkt olarak LOH’a alınır. E peki 85 kilobytetan büyük objeler direkt olarak large object heap’a alınıyor. Large object heap de Garbage collection tarafından nadiren kontrol edilen tek gen’li bir heap. Bu büyük boyutlu objelerin long lived olduğuna neye göre karar veriliyor? Belki büyük boyutlu ancak short lived bir hayata sahip? olamaz mı? Garbage collector bu noktada performansı arttırmak için belli varsayımlar ve kabuller yapmak zorundadır.
O zaman Garbage collectionun yaptığı varsayımlar
- Objeler ya short ya da long liveddır.
- Short-lived objeler bellekte allocate edilir ve 1 single collection cycle’da discard edilir.
- 2 collection cycle sonunda hala yaşayan objeler long livedır.
- Small objelerin %90ı short liveddir.
- Large objeler (+85KB) long liveddır.
Peki biz bellek optimizasyonu açısından performanslı kodlar yazmak istiyorsak ne yapmayılız? Kodumuzu bu varsayımları ve kabulleri destekleyecek şekilde yazmalı, Garbage collectorün içini kolaylaştırmalıyız.
- Koleksiyonlara başlangıç değeri vermeliyiz çünkü sınırsız data alabileceğini düşündüğümüz koleksiyonların arkasında da aslında sınırlı sayıda data alabilen arrayler vardır ve kapasiteleri her dolduğunda yeni x2 boyutlu bir array tanımlanır ve datalar bu arraya taşınır. Bu da memory ve performans açısından maliyetlidir. Dolayısıyla koleksiyonumuzda tutacağımız değerlerin sayısını biliyorsak bunu başlangıç değeri olarak tanımlamak yerinde olacaktır.
- Boxing ve Unboxingten kaçınmalıyız çünkü her boxing ve unboxing işlemi stackten heap’e heapten de stacke bir data geçişi olacağından maliyetli bir operasyondur.
- String builder kullanmalıyız. String builder string işlemlerini performanslı şekilde gerçekleştirir.
- Finalizer’dan uzak durmalıyız; Dispose pattern’i uygulamalıyız