.NET Framework’de geliştirme yapanların, “memory” konusunda biraz daha rahat hissetmesini sağlayan ama sanıldığı kadar basit olmayan bir kavramdan bahsetmek istiyorum. .NET Framework ve “memory” diyince zaten hepimizin bildiği Garbage Collector(GC) direkt aklınızda canlanmıştır diye düşünüyorum.
Geçen hafta yaşadığım bir performans sorununu ve GC ile olan ilişkisinden dolayı bir şeyler yazmak istedim. Şimdi direkt problemden bahsetsem çok anlamlı olmayacak. Bu yüzden öncesinde GC nedir ve nasıl çalışıyor biraz bunlardan bahsetmek istiyorum.
Nedir?
Managed platformalarda(.NET, Java…) memory işlemleri bu platformların kendi içinde yönetilir. Biraz daha low-level dillerde ise memory yönetimi geliştirmecinin kontrolündedir. C/C++ ile geliştirme yapmış olanlar malloc() ve free() metodlarını hatırlar. Gerçi çok hatırlamak istemezler sanırım…Neyse…
Basitçe; yazdığımız kodlardaki sınıflar için çalışma zamanında; nesneler için memory’de belli bir alan ayırırız. Nesne ile ilgili işimizi bitirdiğimizde de bu ayrılan yeri tekrar sisteme geri bırakmalıyız ki, uygulamalarımız sorunsuz çalışsın. “Memory Leak” olmak üzere çeşitli memory problemleri oluşmasın. Neyse ki bu işi .NET tarafında GC bizim için hallediyor.
Peki nasıl hallediyor?
.NET’de CLR(Common Language Runtime) ayağa kalktığında, nesneler için, “managed heap” diye bir alan oluşturur. GC’da bu alan üzerindeki nesnelerin kullanımına göre onları yok eder ya da korur. Kullanımı derken, bu nesnelerin yaşamı olarak düşünebiliriz. Bir nesne için, managed heap üzerinden yaratılan alan, referans olarak kullanılıyorsa, o nesne yaşıyordur gibi düşünebiliriz. GC bu referans alanlarına bakarak(buna GC Roots denir) nesnenin yaşamadığı sonucuna varırsa, memory’deki o nesne için olan alanı boşaltır ve başka nesneler için yer açar.
“Managed Heap”, nesnelerin farklı jenerasyonunu tutacak şekilde tasarlanmıştır. Nesneler yaşam sürelerine göre GC tarafından bir jenerasyondan diğerine taşınır. Bu sayede “memory”de daha fazla yaşaması gereken nesneler yaşamlarına devam eder. Ki bütün olayda burada kopuyor aslında. Bu jenerasyonlar Gen0, Gen1 ve Gen2 olmak üzere 3 tanedir.
Generation 0(Gen0)
Adından da anlaşıldığı gibi İlk jenerasyon nesneler burada yaşar. GC’ın en çok çalıştığı jenerasyon Gen0’dır. Mesela bir metod içerisinde tanımladığımız geçici değişkenlerin alanı Gen0’da olur genelde. Metodun içeriği tamamlandığında, GC bu değişkenlerin referanslarını kontrol edip, referans olmadığını anlar ve onların memory’deki alanlarını boşaltır. Çok büyük nesnelerimiz ve referans olarak erişim yoksa, Gen0’da zaten nesnemiz GC tarafından temizlenirler. Yani memory’deki alanı boşa çıkartılır.
Generation 1(Gen1)
Gen0 yani ilk jenerasyondan sağ kalmayı başaran nesneler, Gen0’da yer kalmadığında Gen1’e kopyalanır. Buradaki nesneler, memory’de biraz daha uzun yaşayan nesnelerdir. Aynı şekilde GC çalıştığında burada da GC Roots’lara bakılıp, memory referanslarına göre nesnenin yaşayıp yaşamadığı anlaşılır. Referansı olmayan nesnelerin memory’deki alanları temizlenir. Burada kopyalama kısmı kritiktir. Kullanılan bir nesnenin memory’deki alanının kopyalanması, o alana erişim yapan thread’ler için tehlike oluşturabilir. Bu kopyalama aşamasında, thread’ler özel bir duruma otomatik olarak getirilir ve memory’deki yeni alanları kullanmaları sağlanır.
Generation 2(Gen2)
Gen1’den de sağ kalmayı başaran nesneler Gen2’ye kopyalanır. Gen2’de çok uzun yaşamı olan nesneler olur genelde. Mesela static değişkenler… Gen2’de de GC işlemi olduğunda, nesnenin GC Roots referansları varsa hala, o nesne için memory’deki alanı korunur. Kontrolsüz bir biçimde Gen2’ye kadar bir nesneyi yaşatmayı başardıysanız, performans sorunları yavaş yavaş oluşacaktır. Birazdan geleceğim buraya tekrar.
Bu noktada önemli bir konu var. Nesnelerin uygulama içerisinde referansı olmasa bile, sahip olduğu nesnelere referansların korunduğu zamanlar olabilir. Dolayısıyla memory’de artık olmadığını düşündüğümüz nesneler, GC tarafından tespit edilemeyebilir. Bu da “memory leak” problemi olarak karşımıza çıkar. OutOfMemory Exception’ları var ya, ahaaa da bunlar onlar…
Peki GC nasıl çalışıyor?
GC, tüm thread’lerden ayrı olarak, arka tarafta bağımsız bir thread olarak çalışıyor. “Eyvah…Thread mi?” dediğinizi duyar gibiyim. “Ayrı bir thread, ekstra yük getirir ama…” diye de devam ediyorsunuzdur büyük ihtimal. Ekstra yük getiriyor ne yazık ki. Yazının başında bahsetmiş olduğum problemde aslında bu yükten kaynaklanan bir durumdu.
Ayrıntısına girmeden önce, bu thread nasıl çalışıyor biraz bundan bahsedelim. Bu ayrı thread en düşük öncelikle çalışıyor. Gen0,Gen1 ve Gen2’deki alanları kontrol edip, memory’i temizliyor. Bu işin ne zaman çalışacağını runtime kendi karar veriyor. Genelde Gen0,Gen1 ve Gen2’de yer azaldığı zaman otomatik olarak çalışıyor. Çalışırken diğer thread’leri de duraklatıyor(pause)…Al sana bir tehlike daha. Bunlar ms. seviyesinde olduğundan çok bir şey hissetmiyoruz.(Haaa haaa sen öyle sen…az sonra…) Bir de GC.Collect() metodu var. Bunu çağırarak, hemen GC’ın çalışmasını sağlayabiliriz. Bilinçiz kullanımı çeşitli performans sorunlarına ve uygulama hatalarına sebep olabilir. Aman diyim…
Nesnelerin ideal yaşamında GC, Gen0’da tüm temizliği bitirir. Gen2’ye kadar yaşayan nesneler ve özellikle büyük nesneler artık GC’ı zorlamaya başlar. Managed Heap’i komple kontrol edip, GC Roots’a göre referans kontrolü yapmak bu aşamada biraz maliyetli olmaya başlıyor. CPU kullanımı artıyor. Ve hatta eğer managed heap’de alan kalmadıysa, GC mutlaka yer açmak durumunda olduğu için thread önceliği en yükseğe(Realtime) çıkıyor. Çok yüksek bir CPU tüketimi başlıyor dolayısıyla.
Eee peki problem?
Karşılaştığım problem, GC’ın memory’de yer açmak için çabalamasından kaynaklanan bir CPU problemiydi. Uygulamamız bir noktadan sonra ciddi bir CPU kullanımı yapıyordu. İlk başta neden olduğunu açıkcası çözemedik. Memory tarafında artış oluyor ama GC çalıştığı için bu artışı çok fark edemiyorduk. Kod içerisinde CPU’yu arttırabilecek operasyonlara yoğunlaşıp, her şeyin normal olduğunu görüp fark edemiyorduk. Taa ki dump alana kadar. Dump alınca, gözümüze hoş gelmeyen bazı ip uçları yakaladık. Daha sonra ANTS Memory Profiller(şimdi reklamlar) ile kontrol ettiğimizde memory’de olmaması gereken nesnelerin referanslarının hala bir şekilde durduğunu gözlemledim. Özellikle Gen2’de, 15 dakika diyip de bütün günü kitleyen davetsiz misafirler dikkatimizi çekti. Kod tarafında hemen düzenlemeyi yaptık ve problemin düzeldiğini gözlemledik.
Böyle durumlarda Dump dışında, uygulama sunucularındaki “Performance Counter” değerlerini de kontrol etmekte fayda var. GC’ın CPU’da geçirdiği zaman, Heap’deki byte değerleri gibi farklı değerlere ara sıra göz atmak gerekli.
Umarım faydalı bir yazı olmuştur. Benzer problemleri ya da farklı GC problemleri yaşanlarınız olursa, yorum olarak paylaşabilirse sevinirim bu arada.