Derinlemesine Garbage Collector

Merhabalar,

Araştırmalarım sonucunda .Net dünyasında anlaşılması itibariyle kanayan yaralarımızdan biri olan Garbage Collector’a ait Türkçe pek bir kaynak bulunmamasından ötürü bu konu hakkında Türkçe bir çalışma yapmak istedim. Araştırmalarım süresince edindiğim bilgileri topladım kendi yorumlarımı da katarak böyle bir makale yazmaya karar verdim.

Uyarı: Uzun bir yazı olacak, yanında çay, kola, kahve gibi enerji verici veya dinç tutan şeyler eşliğinde okumanızı tavsiye ederim:)

Bir Managed(Yönetilebilir) process içinde iki çeşit heap vardır. Native ve Managed heap. Native heap VirtualAlloc Windows Api’si tarafından yönetilir ve işletim sistemi tarafından kullanılan bir heap’tir. Diğeri ise Managed Heap; bu CLR tarafından yönetilir. CLR Garbage Collector’ü çağıran birimdir.

Managed Heap 2’ye ayrılır. Small Object Heap ve Large Object Heap(LOH). Bu heap’ler kendi içlerinde segmentlere ayrılabilir. Her segment de sizin ayarlamalarınıza ve donanımınıza göre değişiklik gösterebilir.

Small Object Heap ise sonrasında Generation(Gen/Nesil)‘lara ayrılır. 3 adet Generation(Gen) vardır. Bunlar Gen 0, Gen 1, Gen 2’dir. Gen 0 ve Gen 1 genelde aynı segmenttedir. Gen 0 ve Gen 1’in olduğu bu segmente Ephemeral Segment(Kısa Ömürlü Segment) adı verilir.

1

Burada Small Object Heap’i A, Large Object Heap’i B temsil etmektedir. Small Object Heap’te bulunan objeler bir yaşam sürecinden geçirilir. CLR 85,000 byte’dan az olan nesneleri SOH’da tutar. Bunlar daima Gen 0 da bulunmaktadır. Bu yüzden .Net’de bellekten alan ayırma çok hızlıdır. Fast Allocation(bellekte hızlı yer açma) başarısız olursa, yine Gen 0 üzerinden bir alana konumlanma işlemi gerçekleştir. Eğer ki bu konumlanma da gerçekleşmezse Allocator üzerinde bulunduğu nesilin sınırlarını genişletmeyi dener. Bu da yetersiz olursa Gen 0 için Garbage Collection tetiklenir.

Objeler hayatlarına daima Gen 0’dan başlarlar. Nesne hayatına devam ettikçe Garbage Collector her Collect işlemine geldiğinde bu yaşayan nesneleri bir üst Generation’a taşır.

Garbage Collection gerçekleştiği anda aynı zamanda segment içinde bir sıkıştırma operasyonu(Compaction) da denenir. Eğer ki sıkıştırma yetersiz kalırsa, segment’lerin sınırları değiştirilmeye başlanır.

2

Örnek olarak görüldüğü gibi Gen 0 biraz daha küçülerek Gen 1 ve 2 segment alanları artmış oldu. Bu gibi durumlarda nesneler yer değiştirmez, sadece sınırlar değişir.

Sıkıştırma işlemi maliyetli bir işlemdir. Eğer ki GC sıkıştırma işlemini tetiklerse, sınırlar değişebileceğinden o anda segment üzerinde varolan nesnelerin adreslerini kaydırması/değiştirmesi gerekebilir. Bunu yapabilmek için bazı Thread’lerin Pause/durma konumuna geçirilmesi gerekebilir. Bu yüzden Compaction(Sıkıştırma) işlemini GC her zaman yapmaz, topladığı istatistikler doğrultusunda doğru andaysa eğer yapar.

Bir nesne eğer ki Gen 2’ye ulaşmışsa, hayatını sona erdirecek olan Callback tetiklenene kadar orada kalır. Fakat bu Gen 2 ye giren orada sonsuza kadar kalabilir veya Gen 2 sonsuza genişleyebilir demek değildir. Eğer Gen 2’de hiç nesne kalmadıysa GC bu segmenti işletim sistemine geri döndürür veya diğer Generation’ların bir parçası olmasını sağlar. Fakat bu ancak Full Garbage Collection esnasında gerçekleşir.

Yaşayan Obje ne demektir?(Alive Object): Eğer ki GC nesnenin kökleriyle nesne grafının bir ucuna ulaşabiliyorsa bu obje yaşıyor demektir. Kök(root) dediğimiz yapı static değişkenler olabilir, thread’lerin üzerindeki stackler olabilir veya finalizer kuyrukları olabilir. Her nesne root barındırmaz, fakat rootu(kökü) olmamasına rağmen bir nesne Gen 2’de ise Gen 0’a yapılan bir Collect işlemi Gen 2’deki nesneyi de Collect etmez. Gen 2 Full Collection’u bekler.

Eğer Gen 0 dolmaya başlamaz ve sıkıştırma yapılamaz ise bu durumda GC yeni bir Segment yaratır. Bu yeni segmentin içinde Gen 1 ve Gen 0 olur. Gen 1 ve Gen 0’ın ayrıldığı bir önceki segment ise tamamen Gen 2’ye bırakılır. Bu allocate işleminin asıl amacı Gen 0’ı boşaltmaktır. Gen 0 boşaltılırken bütün sahip olduğu nesneler Gen 1’e aktarılır/terfi ettirilir aynı Gen 1’den Gen 2 ye geçiş gibi. Bu geçişin doğru ve eksiksiz olması önemsenmez. Sonunda segmentler şöyle bir hâl alır.3Eğer Gen 2 genişlemeye devam ederse birden fazla segmente ayrılabilir. LOH’de aynı zamanda genişlerse segmentlere ayrılabilir. Bu segment ayrım işlemi ne kadar olursa olsun Gen 0 ve Gen 1 daima ve daima aynı segmentte olmak zorundadır.

Large Object Heap(LOH) farklı kurallarla yönetilir. 85,000 byte’dan büyük olan nesneler otomatik olarak buraya alınır ve nesilsel olarak aktarım sürecine girmezler.

Performans nedenlerinden dolayı LOH içerisinde bir sıkıştırma(Compact) işlemi yapılmazdı, fakat .Net 4.5.1’den sonra istenildiği anda(on-demand) yapılabilir hale geldi. Gen 2’de kullanılmayan alanların re-allocate edilebilir hale gelmesi, diğer segmentlere paylaştırılması gibi, LOH’un sahip olduğu alan da kullanılmadığı durumda diğer segmentlere paylaştırılır. Fakat idealde LOH’un collect edilip fazlalık alanlarının paylaştırılması gibi bir durum iyi değildir. LOH’dan bellek istemek tercih edilmemelidir.

Garbage Collector LOH içinde en iyi yeri bulup alan ayıracağı zaman bunu özgürce yapar. Bunu yaparken de fragmentasyonu önlemesi ve minimum düzeyde tutması gerekmektedir.

Not: LOH’a debug atarsanız şunları görebilirsiniz
1- 85,000 byte’dan küçük nesneler
2- LOH’un kendisinin de 85,000 byte’dan küçük olabileceği.
Bu CLR ile ilgili bir durumdur, ignore edebilirsiniz.

GC eğer bir nesil üzerinde Collect işlemi yapıyorsa, alt nesiller de collect edilir. Yani GC Collect ettiği nesil ve altını daima Collect eder. Yani Gen 1 collect ediliyorsa Gen 0’ın da collect edileceği kesindir. Gen 2 collect ediliyorsa Gen 1 ve Gen 0’ın da aynı şekilde. Gen 2 collect ediliyorsa LOH’da collect edilir. Gen 0 ve Gen 1 collect işlemi gerçekleşirse, uygulama collection esnasında durdurulur. Gen 2 collect işlemi esnasında ise collection işlemin bir kısmı, GC ayarlarına bağlı olarak background thread olarak gerçekleşir.

Garbage Collection’un 4 fazı vardır bunlar:

1-Suspension: Bütün yönetilir threadler GC işleminden önce durdurulur.

2-Mark(İşaretleme): Her bir kökten başlayarak, GC her nesne referansını dolaşarak “görüldü” olarak işaretler.

3-Compact(Sıkıştırma): Bellek parçalanmasını(memory fragmentation) önlemek, referanslı nesneleri birbiri ardına koyar ve adres değişimlerini günceller. Bu işlem SOH’de olur ve kontrol edilemezdir GC karar verir. LOH de ise bu böyle olmaz, on-demand şeklinde compact(sıkıştırma) işlemi yapılır.

4-Resume(Devam): Bütün bekletilen threadler yeniden devam etmeye başlar.

Mark(İşaretleme) fazı segmentler içindeki bütün nesneleri dolaşmaz esasında. Collect edeceği nesilin içindeki ve alt nesillerdeki objeleri dolaşır. Örnek olarak Gen 0 collect edilecekse sadece Gen 0 nesneleri dolaşılır. Gen 1 collect edilecek ise Gen 1 ve Gen 0. Gen 2 collect edilecek ise Gen 2, Gen 1 ve Gen 0; bu işleme de Full Collection demiştik. Anlaşılacağı üzere Full Collection maliyetli bir işlemdir. Bazen Gen 0 içindeki bir nesnenin Gen 2’de bir root(kök)’u olabiliyor. Bu GC için üst nesille gitme maliyeti doğurur, fakat Full Collection kadar maliyetli bir dolaşma işlemi değildir.

Collect işlerinin bazı sonuçları vardır.

  • Bir Garbage Collection’un aldığı süre, ne kadar nesnenin yaratıldığına değil, collect edilen nesiller içindeki canlı nesnelere bağlıdır. Mesela milyonlarca nesnenin Collect edileceği bir süreç düşünelim. Eğer ki bu milyonlarca objenin root referansıyla bağlantısını keserseniz, GC için çok basit bir iş haline dönüşür ve zaman almaz.
  • GC geliş sıklığı bir nesilde ne kadar bellek işgal edilebilir sorusuna bağlıdır. Eğer işgal edilebilir eşik aşıldıysa, GC o nesil için tetiklenir. Bu Threshold(eşik) değeri sürekli değişebilir ve GC uygulamanıza göre kendini adapte edebilir. Eğer ki bir nesil üzerinde GC yapmak uygulama için faydalıysa, GC işlemi sıklaşacaktır veya tam tersi gittikçe azalabilir. GC’un uygulamanızdan bağımsız olarak bir diğer tetikleyicisi ise makinenizdeki toplam kullanılabilir bellek alanıdır.

Bu maddeleri düşündüğümüzde sanki GC bizden bağımsız hareket ediyormuş ve sonuçlarına katlanmamız gerektiği gibi bir sonuç ortaya çıkabilir ama bu doğru değildir. Çok iyi optimizasyon ve ayarlamayla bunu kontrol altına alabiliriz.

Genelde GC kendini sizin donanım yapınıza, kullanılabilir kaynaklarınıza ve uygulama davranışlarınıza göre ayarlar. GC için size sadece High-Level ayarlar sunulur ve bunları geliştirdiğiniz uygulamanızın tipine göre değiştirilebilirsiniz

İki tip GC vardır, bunlar:

  • WorkStation GC
  • Server GC

Uygulamanızı geliştirirken en önemli seçim WorkStation GC mi yoksa Server GC mi olur. WorkStation GC varsayılan GC modudur. Bütün GC operasyonları tetikleyici threadle birlikte aynı thread üzerinde aynı öncelikle gerçekleşir. Küçük uygulamalar veya tek işlemcili bilgisayarlarda bu ayarlar pek önem taşımaz.

Server GC her mantıksal işlemci veya çekirdek için bir thread başlatır. Bu threadler en yüksek öncelikte koşarlar. GC isteği gelene kadar da suspend(askıya alınmış) durumda olurlar.

.Net de CLR her işlemci için ayrı bir heap alanı tutar. Bunlar SOH ve LOH’tan oluşur. Uygulamanız açısından bu önemli değildir ve uygulamanız bunu bilmez. Ona göre kullandığı heap’ler tekmiş gibi görünür ve hangi obje gerçekten hangi heap’te, farklı heaplerde bulunan nesnelerin birbiri arasında referans var mı ilgilenmez.

Birden fazla heap’in olmasının faydaları şunlardır:

  • Garbage Collection paralel olarak çalışabilir. Her GC thread’i bir tane heapi collect edebilir ayrı ayrı. Bu yaklaşım workstation GC’den kat kat daha hızlıdır.
  • Bazı durumlarda allocation işlemleri de hız kazanır, özellikle allocationların bütün heaplere yayıldığı Large Object Heap üzerinde.

Server GC şu şekilde aktif hale getirilebilir.

4

Peki Server GC mi WorkStation GC mi kullanılmalı kararını neye göre vermelisiniz ?

  • Server GC
    • Eğer ki uygulamanız multi-processor bir makinede çalışıyor ve sadece sizin uygulamanıza özel bir makineyse o zaman cevap net olarak Server GC’dir

Eğer bir makine üzerinde birden fazla uygulamayı barınrıdıyorsanız, bu seçimi yapmak bu kadar kolay değildir. Mesela Server GC, Garbage Collection işlemi için çok fazla yüksek öncelikli(High Priority) thread oluşturduğundan, makinenizdeki her uygulamanın bunu yapması uygulamaların birbirini negatif yönde etkilemesine ve thread scheduling yapısında conflicting(çakışmalara) sebep olacaktır. Bu gibi durumlarda WorkStation GC en mantıklı seçenektir. Eğer bu durumda da Server GC kullanılmak isteniyorsa uygulamaları belirli işlemcilere sabit kılmak gereklidir. CLR böylece uygulamanın kullandığı Heap’i sadece o işlemci için yaratacak ve GC işlemini sürdürecektir.

Background GC

Background GC/Arkaplan GC Gen 2 nin nasıl collect edileceğiyle ilgilidir. Gen 0 ve Gen 1 ön planda(foreground) işler. 0 ve 1 collect edilirken daha önce bahsettiğim gibi thread’ler pause olur.

Background GC’de bir adet GC thread’i olur. Aynı zamanda Server GC de kullanılıyorsa; GC’den sorumlu mantıksal/fiziksel işlemci başına bir adet thread daha olacak demektir ki bu iki adet threadin varlığı anlamına gelir. Bu bir problem değildir. Garbage Collection işlemi, threadlerinizin çalıştığı esnada gerçekleşebilir. GC isteği Gen 0 ve Gen 1 için bir thread pause işlemi başlatacağı zaman Background GC thread’i de pause/askı durumuna geçirilir. Server GC thread’i Gen 0 ve Gen 1 i temizler. Background GC askıya alındı çünkü o yalnız Gen 2 ile ilgilidir.

Eğer Workstation GC kullanıyorsanız Background GC default olarak etkindir. Ayrıca .NET 4.5’la birlikte Server GC kullanımlarında da default olarak etkin halde başlamaktadır, fakat bu ayarı değiştirebilirsiniz.

Bu ayar Background GC’yi devredışı bırakacaktır.

5

Esasında bu işlemi yapacak durumlarınız çok sınırlıdır. Eğer ki uygulamanızda disposal mekanizmasını siz yönetiyorsanız ve GC’nin CPU time’ınızı yemesini istemiyorsanız ve potansiyel bellek-memory allocation full tehlikesini göz ardı ediyor veya yönetiyorsanız bu ayarı bu şekilde kapatabilirsiniz.

Low Latency Mode

Eğer zaman zaman uygulamanızda kritik performans gereksinimlerine ihtiyaç duyuyorsanız. GC’ye Gen 2 collectionlarını devredışı bırak ayarını yapabilirsiniz. Diğer ayarlarınıza da bağlı olarak, GCSettings.LatencyMode değişkenini şunlara setleyebilirsiniz:

  • LowLatency: Sadece WorkStation GC’de. Gen 2 collection’larını devredışı bırakır.
  • SustainedLowLatency: WorkStation ve Server GC’de. Full Gen 2 collectionlarını devredışı bırakır, fakat Background Gen 2 collectionları devam eder. Bu ayarın çalışabilmesi için Background GC’nin açık olması gerekir.

Bu iki modun kullanımı managed heap(yönetilebilir heap)’in alanını büyük ölçüde genişletir. Çünkü Compaction(Sıkıştırma) da devredışı kalmaktadır. Eğer uygulamanız çok fazla bellek tüketiyorsa bu ayardan kaçınmalısınız.

Bu latency modlarına girmeden önce bir Gen 2 collection’u yapmak iyi olabilir. GC.Collect(2, GCCollectionMode.Forced). Latency modunu bitirdikten sonra tekrar aynı kodu çağırarak bi Gen 2 collection’u daha yapılmalıdır.

Bu özellik çok ileri seviyedir ve default olarak kesinlike tercih edilip kullanılmamalıdır. Eğer hiç interruption(kesilme/duraklama) almadan uzun zaman yaşamasını istediğiniz bir uygulamanız varsa bunu tercih edebilirsiniz. Örnek olarak Stok Ticareti veya stoklarla ilgili işler yapan bir uygulamayı ele alalım. Marketin çalışma saatleri içinde full garbage collection yapmak istemezsiniz. Market kapandığında, LowLatency modunu da kapatarak marketin açılma saatlerine kadar Full GC yapabilirsiniz.

Şu senaryolar size uyuyorsa bu ayarı kullanın:

  • Full Garbage Collection’un gecikmesi uygulamanızda kabul edilebilir bir zaman değilse
  • Uygulamanın bellek kullanımı kullanılabilir bellekten çok düşükse
  • LowLatency modunu kapattığınızda, manuel olarak Gen 2 collect yaptığınızda veya uygulamayı yeniden başlattığınızda kendi başına da uzun yaşabiliyorsa.

Diğer koşullarda bu ayar için 2 kere düşünün. Düşündükten sonra bile kullanmaya karar verdiniz diyelim, öncelikle bunu bir test aşamasından geçirin hakkaten size faydalı olabileceğini görün. Sonrasında kesin olarak uygulamaya geçirebilirsiniz. Aksi takdirde çeşitli problemlerle karşı karşıya kalabilirsiniz. Gen 2 boşaltılmadığı için Gen 1 ve Gen 0 daki birikmeler Gen 2 ye terfi ettirilir. Full Garbage collection tetiklenmediği için allocation problemleri ve Full Collection tetiklenmemesi gibi sorunlar ortaya çıkabilir.

Fakat şunu belirtmekte fayda var. Garbage Collector ne olursa olsun OutOfMemoryException fırlatmayı tercih etmez, tehlike anında ayarlarınıza bakmaksızın Full Garbage Collection başlatır.

Yer Ayırma Oranını Azaltın

Uygulamanızın işgal ettiği bellek alanını azaltmak istiyorsanız, Garbage Collector’un üzerindeki baskıyı da azaltmanız gerekmektedir. Bu biraz yaratıcılık gerektirebilir ve tasarım kurallarınızla çakışabilir.

Bir bellek alanını işgal edecekseniz şunları değerlendirmeniz gerekmekte:

  • Gerçekten böyle bir nesneye ihtiyacım var mı ?
  • Fazlalık olan propertyler, fieldlar var mı ?
  • Dizilerin boyutunu düşürebilir miyim ?
  • Primitif tipleri daha küçüğe indirgeyebilir miyim? Örnek olarak Int64’ten Int32’e
  • Kullandığım nesneler nadir durumlarda initialize oluyor mu, eğer öyleyse lazy olarak davranabilir miyim?
  • Yazdığım bazı class’ları struct’a dönüştürebilir miyim ? Çünkü struct stack üzerinde yaşar, heap’ten daha maliyetsizdir. Ya da başka bir nesnenin parçası haline getirebilir miyim?
  • Çok bellek mi harcıyorum acaba? Yoksa küçük bir parça mı?

Yüksek performanslı bir kod yazmanın Garbage Collector ile ilgili olan temel kurallarından biri ve aynı zamanda GC’nin tasarımının altında yatan nedenlerden:

Gen 0’daki nesneleri collect et ya da hiç collection yapma.

Diğer bir deyişle, sürekli ömrü çok kısa nesnelerle iş yapmaktır. Garbage Collection Generationlar arasında gittikçe maliyeti de artmaktadır. Gen 0 ve 1’in daima fazla Gen 2’nin ise çok az olmasını istersiniz. Gen 2’yi Background GC halletse bile, CPU’ya bu yükü bindirmek istemezsiniz.

Daima Gen 1 collection’undan kaçınmalısınız. Gen 1’deki nesneler Gen 2’ye geçme eğilimindeki nesnelerdir. Gen 1 bir nevi Gen 2 buffer’ı olarak düşünülebilir.

Bir nesnenin hayatı ne kadar kısa olursa, bir üst Generation’a terfi ettirilme şansı da o kadar az olur. Bu yüzden objelere ihtiyaç duyduğunuz noktaya kadar yer ayırmayın/belleğe almayın, initialize etmeyin. Fakat bir kaç ayrıcalıklı durum olabilir, örnek olarak objenin yaratılma maliyeti çok fazlaysa eğer daha önceden diğer işlemlere engel olmadığı bir noktada yaratılabilir.

Eğer ki ihtiyacınız olan nesneyi methodlar/operasyonlar arasında paylaşıyorsanız, methodları birbirine yaklaştırın veya nesneyle ilgili işleri çabuk bitirmeye çalışın ki GC bir an önce boşa çıkmış olan nesneyi collect etme işlemini gerçekleştirebilsin.

Dallanma Derinliğini Azaltmak

Daha önceki başlıklarda belirttiğim üzere GC tamamiyle referans sayma/takip etme mantığıyla çalışmaktadır. Bunu yaparken de paralelizm kullanır. Fakat eğer bir GC thread’i bir referans grafı üzerinde uzun zaman alırsa, diğer GC threadleri uzun süren parçayı beklerler. Fakat CLR’ın son sürümlerinde bu çok daha az endişe edilir bir noktaya geldi, thread’ler “iş çalma(work-stealing)” algoritmasıyla bunu aşmaktadırlar. Fakat her ne kadar da aşılsa, siz yine de uzun dallanmış referanslardan uzak durun, eğer performansı artırmak istiyorsanız bakmanız gereken noktalardan birisi de burasıdır.

Objeler Arasındaki Referansların Sayısını Azaltın

Birbirlerine referans olan objelerin collect edilmesi uzun sürer. Eğer GC çalıştığı esnada duruş/pause sayısı fazla ise bu nesne grafının uzun olduğu olduğu anlamına gelir. Ve bir diğer konu ise bu nesnelerin yaşam sürelerini kestirmek de kolayca mümkün olamamaktadır. Bu yüzden karmaşıklıktan kaçıp basit kod akışları içinde kodlarımızı yazmak Garbage Collector için de faydalı olacaktır.

Farklı Generation’ların(nesillerin) birbirini referans almaması konusuna özen göstermek gereklidir. Yeni nesnelerin eskileri referans alması da aynı şekilde tehlikelidir. Örnek olarak Gen 0’daki bir nesne Gen 2’deki bir nesneyi referans alıyorsa, GC her ne zaman Gen 0’ı collect etmeye geldiğinde Gen 2 referansı için bir scanning(araştırma) maliyeti çıkar, hâlâ birbirlerine referansları var mı yok mu diye. Full Garbage Collection kadar maliyetli değildir ama kaçınılması gerekir.

Pinning(Sabitleme)’den Kaçının

Pinleme; yönetilebilir(managed) bellek referanslarını native(yerel,işletim sisteminin ilişkili olduğu) kodlara geçebilmeyi sağlar. Genelde dizi ve stringler geçilir. Eğer ki native kod ile bir çalışmanız veya ihtiyacınız yoksa pinlemeye de ihtiyacınız yok demektir.

Bir objeyi Pinlemek demek, GC’nin o objeye dokunmaması/taşıyamaması demektir. Pinleme işlemi aslında maliyetli değildir, fakat GC’de fragmentasyon(parçalanma) olasılığını artırıcı şekilde bir etkisi vardır.

Pinleme; explicit(açık) ve implicit(kapalı) olarak ikiye ayrılır. Explicit pinleme GCHandle kullanılarak GCHandleType.Pinned ile yapılır ya da fixed anahtarı da kullanılabilir, fakat fixed i kullanabilmeniz için kodun unsafe modda olması gerekmektedir. fixed ve handle kullanmak birbiriyle aynı işi görür, örnek olarak disposable bir instance’ı using statement’i içinde kullanmakla Dispose methodunu explicit olarak çağırmak gibi.

Implicit pinleme daha genel bir kullanıma sahiptir ve kaldırması da zordur. Implicit pinning genelde Platform Invoke (P/Invoke) çağrılarına geçilir. CLR da pinned objelere sahiptir ayrıca ama bunlar önemsenecek düzeyde değildir.

Pinlenen nesneler Gen 2 veya Large Object Heap(LOH)’da kalmaktadır.

Eğer Pinlemeyi kodunuzdan çıkaramıyorsanız, daha önce bahsettiğim şu kuralı uygulamaya çalışın: Nesnelerin yaşamı daima kısa olmalıdır, bir nesneyi çağıran methodlar birbirinden uzak konumlanmamalıdır ve scope olabildiğince küçük kalmalıdır. Böylece Garbage Collector işi biten, ömrü kısa nesneleri bir an önce toplasın.

Sonlandırıclardan(Finalizers) Kaçının

Tek kural: Eğer ki hakkaten çok gerekli değilse, asla bir Finalizer implement etme. Finalizer’ler GC tarafından tetiklenen, yönetilmeyen kod parçalarını temizlemeye yarayan kod bloklarıdır. Finalizer’lar Garbage Collector toplama işlemini tamamlayıp, nesneyi ölü olarak ilan ettikten sonra tek bir thread ile çağırılırlar, paralel değildirler, birer birer işlenir. Yani bu demektir ki, eğer classınız bir finalizer implemente ederse, Garbage Collection işleminden sonra bile nesnenin hayatta kalacağını garanti edersiniz. Bu GC’nin verimliliğini azaltır ve program genel olarak CPU time’ı tüketmeye başlar CPU kaynakları Finalizer’leri çağırmakla yükümlü olur.

Eğer bir finalizer implemente etmişseniz explicit cleanup’u aktif hale getirmek için IDisposable interface’ini de implemente etmeniz gerekmektedir. Ve Dispose methodunda GC.SuppressFinalize(this) çağrısını yapmanız gerekmektedir. Bu çağrı GC collection’unundan sonra CPU’nun dolaşacağı Finalizer kuyruğundan sizin this ile belirttiğiniz classınızı çıkarmaya yarar. GC’nin gelip toplama yapmasından önce Dispose’u çağırarak böylece classınızı clean etmiş yani collect işlemini birnevi siz gerçekleştirmiş olursunuz.

Bir Finalizer örneği;

internal class Foo : IDisposable
{
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~Foo()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {

        if (disposing) { this.managedResource.Dispose(); }
        // Yönetilmeyen kaynakları temizle
        UnsafeClose(this.handle);
        // Eğer base class’ınız var ve o da IDisposable implement etmişse
        // base.Dispose(disposing);
        // çağrısını yapmanız gerekmektedir.
    }
}

Bazıları Finalizer’lerin çalışacağının garanti olduğunu düşünebilir. Fakat bu her zaman böyle değildir. Eğer ki uygulamanız dışardan terminate/shutdown edildiyse, finalizer’lere ait bir çalışma süre sınırı vardır. Dahası, bahsettiğim üzere finalizer’ler bir kuyrukta bekletilip CPU kaynaklarıyla sıralı olarak koşuldu için, eğer ki kuyruktaki Finalizer’lerden birisi sonsuz döngüye girmişse, bu zaman aşılacağından kuyruktaki diğer Finalizerler hiç çalışmayabilir. Bu yüzden uygulamanızın collection yapısını finalizer’lere güvenerek inşa etmeyin.

Büyük Bellek İşgallerinden Kaçınma

Daha önce bahsettiğim gibi bir nesnenin LOH’a geçebilmesi için gereken boyut 85,000 byte dı. Bu sınırdan büyük olan nesneler LOH’a gönderiliyordu. Fakat bu denli büyük nesneler neden var, neden programımızda barınıyor gibi soruların da cevabını bulmak gerekiyor. Eğer ki böyle bir bellek kullanımı varsa önce bunun araştırılması performans için başlanacak yerlerden birisidir.

Buffer(Tampon)’ları Kopyalamaktan Kaçınma

Nesneleri kopyalama işlemlerini sıkça yapabiliyoruz, fakat eğer kopyalamanın dışında farklı yollarla veriyi ifade edebiliyorsa GC’nin üzerine düşen yükü azaltmış oluruz. Örnek olarak MemoryStream’i ele alalım.

6

Eğer ki MemoryStream in bir parçasına ihtiyacınız varsa bunu kopyalamadan, ArraySegment oluşturarak sağlayabilirsiniz. Kopyalama işlemi CPU harcamaz fakat GC’ye bir yüktür.

Garbage Collector’ü Toplama Yapmaya Zorlama Durumları

Normal şartlarda GC’yi toplama işlemi yapmaya zorlamamalısınız. Çünkü GC kendisi bu işi üstlendiğinden uygulamanızın da bir istatistiğini çıkarmaktadır. Bu istatistiğe göre ne zaman geleceğini ve optimum süreleri hesaplar. Eğer ki Force Collect yapılırsa bu istatistiki verinin dışına çıkacağı için, bellek yönetimi kararsızlaşabilir. Dolayısıyla zorunda olmadıkça bu işlemi yapmamak gerekir.

Fakat bazen yüksek performans almak adına uygulamanızın türüne ve işleyişine de bağlı olarak Force Collect gerçekleştirilebilir. Daha doğrusu şöyle düşünülebilir:“Şimdi Force etmek daha sonra GC’nin kendisinin gelmesinden daha verimlidir” sonucu varsa GC Force işlemi gerçekleştirilebilir. Burada Foce Collect olarak düşündüğümüz collection modu Full Collection’dur. Diğer yandan Gen 0 ve Gen 1’in sık sık collect edilmesi bir problem yaratmaz, Gen 0’ın çok fazla yer işgel etmesini önlemek adına.

Eğer şu durumlarınız varsa Force Full Collect yapabilirsiniz:

  • LowLatency mod aktifse
  • Uygulamanızdaki allocation yapısı genelde büyük objelerden ve long live objelerden oluşuyorsa Gen 2 yi aktif olarak kullanıyorsunuz demektir, bu durumda force collection yapabilirsiniz
  • Fragmentation çok fazlaysa ve fragmentasyon yüzünden sıkıştırmayı tetikleyecek duruma gelindiyse Force kullanılabilir.

GC’ye Collect etme işlemini dışardan 3 farklı türde yaptırabilirsiniz.

  • Default: Varsayılan, Forced
  • Forced: Collection’a hemen başla
  • Optimized: GC’ye en uygun zamanı seçmesi için seçenek tanı.

7

Örnek kullanım senaryosu:

Kullanıcı Query’lerini karşılayan bir sunucu düşünün. Her saatte 1 GB verinin tekrar yükleme yapıldığını ve bu re-loading verinin de eskisinin yerini aldığını düşünelim. Bu noktada yeni veri geldiğinde eski veriye ait değerlerin allocate olmadığına emin olmamız gerekir. Dolayısıla bu noktada Full Forced GC’yi her request sonrasında çalıştırmak mantıklı olacaktır. Böylece yeni verilerin tamamı ya Gen 0’a toplanmış ya da ait olduğu Gen 2’ye getirilmiş demektir.

Large Object Heap’i Sıkıştırma

Zamanla LOH fragmentasyona uğrayabilir. Daha önce bahsettiğim gibi .Net 4.5.1’den sonra on-demand compaction olayı geldiği için GC’ye bir sonraki toplama işleminde LOH’u da sıkıştır emrini geçebilirsiniz.

8

LOH’un büyüklüğüne bağlı olarak bu işlem biraz sürebilir. Bir kaç saniye civarında. Bu durumlarda uygulamanızı geçici bir stand-by konumuna almak isteyebilirsiniz. Bu ayar setlendiğinden itibaren bir sonraki Full GC’yi etkiler ve Full collection yapılıp compaction tamamlanınca GCSettings.LargeObjectHeapCompactionMode tekrardan GCLargeObjectHeapCompactionMode.Default moda geçiş yapar.

Toplama(Collection) İşlemi Gerçekleşmeden Haberdar Olmak

Full GC gerçekleşmeden önce bu olayın yaklaştığına dair bir bildirim alabilmek mümkündür. Eğer yaklaşıyorsa uygulamayı pause durumuna geçirebilirsiniz veya başka işler yapabilirsiniz bunun sayesinde. Fakat dikkatli olmak gerekir, bu bildirim sistemi sanki bütün GC’nin sıkıntılarını çözecekmiş gibi görünse de başka sorunları da beraberinde getirebilir. Bu adımı diğer anlattıklarımı denedikten ve düzelttikten sonra yapmanızı tavsiye ediyorum. Eğer şu durumlara sahipseniz:

  • Full GC uygulamanız için aşırı maliyetliyse
  • Uygulamayı durdurabilme gibi bir özelliğiniz varsa (diğer processler işlerine devam edebilir)
  • Normal GC’nin işlemleri durdurması çok maliyetliyse, siz daha önceki bir noktada istediğiniz şeyleri durdurmak istiyorsanız.
  • Zaten Gen 2 toplama süreçleri nadiren yapılan bir işlemdir. Bu bildirime/event’e register olmaya değer diyorsanız.

Full GC’den haberdar olmayı seçebiliriniz.

Bu işlemi gerçekleştirmek için şu adımları izleyebilirsiniz:

  • RegisterForFullGCNotification methodunu çağırarak 2 adet eşik değerini girmek
  • WaitForFullGCApproach methodunu setlemek, bir timeout değeri alır
  • Eğer WaitForFullGCApproach methodu başarılı dönerse, uygulamanız Full GC kabul edebilir bir state/duruma çekilmiş demektir.
  • Şu haldeyken Collect methodunun kontrolü artık sizin elinizdedir ve çağırabilirsiniz.
  • Sonrasında WaitForFullGCComplete bir timeout değeriyle setleyerek devam etmeden önce Full GC bekleme süresini verebilirsiniz.
  • Uygulamanız istekleri tekrar almaya başlayabilir hale gelir
  • GC’nin bildirimlerini istemiyor iseniz CancelFullGCNotification methodunu çağırabilirsiniz. 

Caching İçin Weak Referans Kullanma

Normalde bir nesne CLR’da new’lendiği zaman Gen 0’a atılır ve referansı tutulur. Eğer ki nesneye başka bir nesne üzerinden bir erişim varsa GC bu nesneyi Alive olarak değerlendirir ve collect etmez. İşbu nesne bir hayli büyük ve yaratılması maliyetliyse bazen sürekli Alive durumda kalabilir ve GC’nin allocate alanını daraltır ihtiyaç duyulmamasına rağmen. İşte bu noktada sanki bir Caching mekanizması gibi devreye WeakReferance girer. WeakReference bu büyük nesnelerin referansını tutar orjinal nesneyi birnevi encapsüle eder. Bu encapsüle işleminin amacı nesnenin başka referansları olsa da collect edilebilir kılmaktır.  Kullanırken çok dikkatli olmak gerekir. GC’nin ne zaman geleceği belli olmadığından WeakReference’ların ne zaman collect edileceği nesnelerin ne zaman null’a düşeceği belli olmaz. Dolayısıyla WeakReference bize IsAlive diye bir method sunar. Erişime başlamadan önce bununla kontrol etmeliyiz. Fakat bu da yetmez. IsAlive çağırıldıktan sonra true alsak bile nesnenin o adımdan sonra collect edilmediğinin garantisini veremeyiz. Dolayısıyla nesneye ait scope başlamadan WeakReference’i başka bir nesneye kopyalayıp o kopya üzerinden kontolleri gerçekleştirmek daha güvenlidir.

9

Advertisements

One thought on “Derinlemesine Garbage Collector”

  1. Hocam eline sağlık. Uzun zamandır ertelediyim bellek yönetimi konusu ile ilgili araştırma yaparken rastladım makalene. Gerçektende çok aydınlatıcı bir yazı olmuş . Benim için karanlık olan bir çok konu hakkında artık bir fikir sahibiyim. Teşekkürler!

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s