Çöp Toplayıcı temel bilgileri ve performans ipuçları

 

Rico Mariani
Microsoft Corporation

Nisan 2003

Özet: .NET çöp toplayıcı, bellek kullanımı iyi olan ve uzun süreli parçalanma sorunları olmayan yüksek hızlı bir ayırma hizmeti sağlar. Bu makalede çöp toplayıcıların nasıl çalıştığı açıklanır ve ardından çöp toplama ortamında karşılaşılabilecek bazı performans sorunlarını ele alır. (10 yazdırılan sayfa)

Aşağıdakiler cihazlar için geçerlidir:
   Microsoft® .NET Framework

İçindekiler

Giriş
Basitleştirilmiş Model
Çöp Toplama
Performans
Son -landırma
Sonuç

Giriş

Atık toplayıcıyı nasıl iyi kullanacağınızı ve çöp toplama ortamında çalışırken karşılaşabileceğiniz performans sorunlarını anlamak için, çöp toplayıcıların nasıl çalıştığına ve bu iç çalışmaların çalışan programları nasıl etkilediğine ilişkin temel bilgileri anlamak önemlidir.

Bu makale iki bölüme ayrılmıştır: İlk olarak basitleştirilmiş bir model kullanarak ortak dil çalışma zamanının (CLR) çöp toplayıcısının yapısını genel olarak ele alacağım ve ardından bu yapının bazı performans etkilerini tartışacağım.

Basitleştirilmiş Model

Açıklayıcı amaçlarla, yönetilen yığının aşağıdaki basitleştirilmiş modelini göz önünde bulundurun. Bunun gerçekte uygulanan şey olmadığını unutmayın.

Şekil 1. Yönetilen yığının basitleştirilmiş modeli

Bu basitleştirilmiş modelin kuralları aşağıdaki gibidir:

  • Tüm çöp toplanabilir nesneler, bir bitişik adres alanı aralığından ayrılır.
  • Yığın nesillere ayrılır (daha sonra bu konuda daha fazla), böylece yığının yalnızca küçük bir bölümüne bakarak çöplerin çoğunu ortadan kaldırmak mümkündür.
  • Bir nesil içindeki nesnelerin tümü kabaca aynı yaştadır.
  • Daha yüksek numaralı nesiller, yığının eski nesneler içeren alanlarını belirtir; bu nesnelerin kararlı olma olasılığı çok daha yüksektir.
  • En eski nesneler en düşük adreslerde, yeni nesneler ise artan adreslerde oluşturulur. (Yukarıdaki Şekil 1'de adresler giderek artıyor.)
  • Yeni nesneler için ayırma işaretçisi, kullanılan (ayrılan) ve kullanılmayan (serbest) bellek alanları arasındaki sınırı işaretler.
  • Ölü nesneler çıkarılarak ve canlı nesneler yığının alt adres ucuna doğru kaydırılarak düzenli aralıklarla yığın sıkıştırılır. Bu, diyagramın en altında yeni nesnelerin oluşturulduğu kullanılmayan alanı genişletir.
  • Bellekteki nesnelerin sırası, iyi bir yerellik için oluşturuldukları sırada kalır.
  • Yığındaki nesneler arasında hiçbir zaman boşluk yoktur.
  • Boş alanın yalnızca bir kısmı işlenir. Gerektiğinde, ** ayrılmış adres aralığındaki işletim sisteminden daha fazla bellek alınır.

Çöp Toplama

Anlaşılması en kolay toplama türü, tamamen sıkıştırılmış çöp toplamadır, bu nedenle bunu tartışarak başlayacağım.

Tam Koleksiyonlar

Tam bir koleksiyonda program yürütmesini durdurmalı ve GC yığınının tüm köklerini bulmalıyız. Bu kökler çeşitli biçimlerde gelir, ancak özellikle yığına işaret eden yığın ve genel değişkenlerdir. Köklerden başlayarak, her nesneyi ziyaret ediyoruz ve biz devam ettikçe nesneleri işaretleyerek ziyaret edilen her nesnede bulunan her nesne işaretçisini takip ediyoruz. Bu şekilde toplayıcı her erişilebilir veya canlı nesneyi bulmuş olur. Diğer nesneler, ulaşılamayanlar artık kınanıyor.

Şekil 2. GC yığınına kökler

Ulaşılamayan nesneler belirlendikten sonra bu alanı daha sonra kullanmak üzere geri kazanmak istiyoruz; Bu noktada toplayıcının amacı canlı nesneleri yukarı kaydırmak ve boşa harcanan alanı ortadan kaldırmaktır. Yürütme durdurulduysa, toplayıcının tüm bu nesneleri taşıması ve tüm işaretçileri düzelterek her şeyin yeni konumunda düzgün bir şekilde bağlanması güvenlidir. Hayatta kalan nesneler yeni nesil sayıya yükseltilir (nesillerin sınırlarının güncelleştirildiği söylenebilir) ve yürütme devam edebilir.

Kısmi Koleksiyonlar

Ne yazık ki, tam çöp toplama işlemi her seferinde yapamayacak kadar pahalı olduğundan, artık koleksiyondaki nesillerin bize nasıl yardımcı olduğunu tartışmak uygun olur.

İlk olarak, olağanüstü şanslı olduğumuz hayali bir olayı ele alalım. Yeni bir tam koleksiyon olduğunu ve yığının güzel bir şekilde sıkıştırıldığını varsayalım. Program yürütme devam eder ve bazı ayırmalar gerçekleşir. Aslında, çok fazla ve çok sayıda ayırma gerçekleşir ve yeterli ayırmadan sonra bellek yönetim sistemi toplama zamanına karar verir.

İşte şanslı olduğumuz yer burası. Son koleksiyondan bu yana çalıştırdığımız süre boyunca eski nesnelerin hiçbirine yazmadığımız, yalnızca yeni ayrılan, sıfır nesil (0. nesil) nesnelerin yazıldığını varsayalım. Böyle bir şey olsaydı, çöp toplama işlemini büyük ölçüde basitleştirebileceğimiz için büyük bir durumda olurduk.

Her zamanki tam toplamamız yerine, tüm eski nesnelerin (1. nesil, 2. nesil) hala canlı olduğunu veya bu nesnelere bakmanın değmeyecek kadar canlı olduğunu varsayabiliriz. Ayrıca, bunların hiçbiri yazılmadığından (ne kadar şanslı olduğumuzu hatırlayın)) eski nesnelerden yeni nesnelere kadar hiçbir işaretçi yoktur. Yani yapabileceğimiz şey, her zamanki gibi tüm köklere bakmak ve herhangi bir kök eski nesnelere işaret ederse, sadece bu köklere göz ardı etmek. Diğer kökler için (0. nesil'e işaret edenler) tüm işaretçileri izleyerek her zamanki gibi devam ederiz. Eski nesnelere geri dönen bir iç işaretçi bulduğumuzda, bunu yoksayarız.

Bu işlem tamamlandığında,0 . nesildeki her canlı nesneyi, eski nesillerden herhangi bir nesneyi ziyaret etmeden ziyaret etmiş olacağız. 0. nesil nesneleri her zamanki gibi kınanabilir ve yalnızca bu bellek bölgesini kaydırarak eski nesneleri rahatsız edilmeden bırakırız.

Şimdi bu bizim için gerçekten harika bir durum çünkü ölü alanın çoğunun büyük bir değişim sıklığının olduğu daha genç nesnelerde olabileceğini biliyoruz. Birçok sınıf, dönüş değerleri, geçici dizeler ve numaralandırıcılar ve diğer yardımcı sınıflar gibi çeşitli diğer yardımcı sınıflar için geçici nesneler oluşturur. Sadece0 . nesile bakmak, nesnelerin yalnızca çok azını inceleyerek ölü alanın çoğunu geri almak için bize kolay bir yol sağlar.

Ne yazık ki, en azından bazı eski nesneler yeni nesnelere işaret edecek şekilde değişmeye bağlı olduğundan bu yaklaşımı kullanacak kadar şanslı değiliz. Böyle bir durumda, onları yok saymak yeterli değildir.

Nesillerin Yazma Engelleriyle Çalışmasını Sağlama

Yukarıdaki algoritmanın gerçekten çalışmasını sağlamak için hangi eski nesnelerin değiştirildiğini bilmemiz gerekir. Kirli nesnelerin konumunu anımsamak için kart tablosu adlı bir veri yapısı kullanırız ve yönetilen kod derleyicisi bu veri yapısını korumak için yazma engelleri olarak adlandırılır. Bu iki ilke, nesil tabanlı çöp toplamanın başarısını merkezi olarak oluşturur.

Kart tablosu çeşitli yollarla uygulanabilir, ancak bunu bir bit dizisi olarak düşünmenin en kolay yoludur. Kart tablosundaki her bit, yığındaki bir bellek aralığını temsil eder; diyelim ki 128 bayt. Bir program bir nesneyi bir adrese her yazdığında, yazma engeli kodunun hangi 128 baytlık öbeğin yazıldığını hesaplaması ve ardından kart tablosunda karşılık gelen biti ayarlaması gerekir.

Bu mekanizmayı kullanıma aldığımızda artık koleksiyon algoritmasını yeniden ziyaret edebiliriz. 0. nesil çöp toplama işlemi yapıyorsak, yukarıda açıklandığı gibi algoritmayı kullanabiliriz ve eski nesillere yönelik işaretçileri yoksayabiliriz, ancak bunu yaptıktan sonra kart tablosunda değiştirilmiş olarak işaretlenmiş bir öbek üzerinde bulunan her nesnede her nesne işaretçisini de bulmalıyız. Bunlara kök gibi davranmalıyız. Bu işaretçileri de dikkate alırsak, yalnızca 0. nesil nesnelerini doğru bir şekildetoplarız .

Kart tablosu her zaman dolu olsaydı bu yaklaşım hiç yardımcı olmazdı, ancak pratikte eski nesillerden gelen işaretçilerin nispeten azı aslında değiştirildiğinden, bu yaklaşımdan önemli bir tasarruf elde edilir.

Performans

İşlerin nasıl çalıştığına ilişkin temel bir modele sahip olduğumuza göre, şimdi de yavaşlamasına neden olabilecek bazı yanlışlıkları göz önünde bulunduralım. Bu, toplayıcıdan en iyi performansı elde etmek için ne tür şeylerden kaçınmamız gerektiği konusunda bize iyi bir fikir verir.

Çok Fazla Ayırma

Bu gerçekten de yanlış gidebilecek en temel şeydir. Çöp toplayıcı ile yeni bellek ayırma işlemi oldukça hızlıdır. Yukarıdaki Şekil 2'de görebileceğiniz gibi, genellikle ayırma işaretçisinin yeni nesneniz için "ayrılan" tarafta yer oluşturmak üzere taşınması gerekir; bundan daha hızlı olmaz. Ancak, er ya da geç bir çöp toplamanın gerçekleşmesi gerekir ve her şey eşit olduğunda, bunun daha sonra gerçekleşmesi daha önce olmasından daha iyidir. Bu nedenle, yeni nesneler oluştururken, tek bir nesne oluşturmak hızlı olsa bile bunu yapmak için gerçekten gerekli ve uygun olduğundan emin olmak istiyorsunuz.

Bu bariz bir öneri gibi görünebilir, ancak aslında yazdığınız küçük bir kod satırının çok fazla ayırmayı tetikleyebileceğini unutmak son derece kolaydır. Örneğin, bir tür karşılaştırma işlevi yazdığınızı ve nesnelerinizin bir anahtar sözcük alanı olduğunu ve verilen sırayla anahtar sözcüklerde karşılaştırmanızın büyük/küçük harfe duyarlı olmasını istediğinizi varsayalım. Bu durumda, ilk anahtar sözcük çok kısa olabileceğinden yalnızca anahtar sözcük dizesinin tamamını karşılaştıramazsınız. Anahtar sözcük dizesini parçalara ayırmak ve ardından normal büyük/küçük harfe duyarlı olmayan karşılaştırmayı kullanarak her bir parçayı karşılaştırmak için String.Split kullanmak cazip olacaktır. Kulağa harika geliyor, değil mi?

Öyle yapmak pek iyi bir fikir değil. Gördüğünüz gibi String.Split bir dize dizisi oluşturacak. Bu, anahtar sözcük dizenizdeki her anahtar sözcük için bir yeni dize nesnesi ve dizi için bir nesne daha anlamına gelir. Yikes! Bunu bir sıralama bağlamında yapıyorsak, bu çok fazla karşılaştırmadır ve iki satırlı karşılaştırma işleviniz artık çok fazla sayıda geçici nesne oluşturuyor. Aniden çöp toplayıcı sizin adınıza çok sıkı çalışmaya başladı ve hatta en akıllı toplama düzeninde bile temizlenecek çok fazla çöp var. Ayırma gerektirmeyen bir karşılaştırma işlevi yazmak daha iyidir.

Too-Large Ayırmaları

Programcılar, malloc() gibi geleneksel bir ayırıcıyla çalışırken, ayırma maliyetinin nispeten yüksek olduğunu bildikleri için mümkün olduğunca az malloc() çağrısı yapan kodlar yazar. Bu, daha az toplam ayırma gerçekleştirebilmemiz için öbekler halinde ayırma ve genellikle ihtiyacımız olabilecek nesneleri tahmine dayalı olarak ayırma uygulamasına dönüşür. Önceden ayrılmış nesneler daha sonra bir tür havuzdan el ile yönetilir ve etkili bir şekilde bir tür yüksek hızlı özel ayırıcı oluşturulur.

Yönetilen dünyada bu uygulama çeşitli nedenlerle çok daha az ilgi çekicidir:

İlk olarak, ayırma işleminin maliyeti son derece düşüktür; geleneksel ayırıcılarda olduğu gibi ücretsiz bloklar aranmaz; Tek yapılması gereken, serbest ve ayrılan alanların taşınması gereken sınırdır. Düşük ayırma maliyeti, havuza almak için en zorlayıcı nedenin mevcut olmadığı anlamına gelir.

İkinci olarak, önceden ayırmayı seçerseniz, elbette acil gereksinimleriniz için gerekenden daha fazla ayırma yapmış olursunuz ve bu da başka türlü gereksiz olabilecek ek çöp toplamaları zorlayabilir.

Son olarak, çöp toplayıcı el ile geri dönüştürdüğünüz nesneler için alan geri kazanamaz, çünkü genel bakış açısından şu anda kullanımda olmayan nesneler de dahil olmak üzere tüm bu nesneler hala canlıdır. Kullanıma hazır ancak kullanımda olan nesneleri elde tutarak çok fazla bellek harcandığını fark edebilirsiniz.

Bu, önceden ayırmanın her zaman kötü bir fikir olduğunu söylemek değildir. Örneğin, belirli nesnelerin başlangıçta birlikte ayrılmasını zorlamak için bunu yapmak isteyebilirsiniz, ancak bunun genel bir strateji olarak yönetilmeyen kodda olduğundan daha az ilgi çekici olduğunu fark edebilirsiniz.

Çok Fazla İşaretçi Var

Büyük bir işaretçi ağı olan bir veri yapısı oluşturursanız iki sorununuz olur. İlk olarak, çok fazla nesne yazma işlemi olacaktır (aşağıdaki Şekil 3'e bakın) ve ikinci olarak, bu veri yapısını toplama zamanı geldiğinde, çöp toplayıcının tüm bu işaretçileri izlemesini ve gerekirse her şey ilerledikçe bunların tümünü değiştirmesini sağlayacaksınız. Veri yapınız uzun ömürlüyse ve çok fazla değişmiyorsa, toplayıcının yalnızca tam koleksiyonlar gerçekleştiğinde (2 . nesil düzeyinde) tüm bu işaretçileri ziyaret etmek zorunda kalır. Ancak, işlem işlemlerinin bir parçası olarak böyle bir yapıyı geçici olarak oluşturursanız, maliyeti çok daha sık ödersiniz.

Şekil 3. İşaretçilerde ağır veri yapısı

İşaretçilerde ağır olan veri yapılarında çöp toplama süresiyle ilgili olmayan başka sorunlar da olabilir. Daha önce de bahsettiğimiz gibi, nesneler oluşturulduğunda ayırma sırasına göre bitişik olarak ayrılırlar. Örneğin, bir dosyadan bilgileri geri yükleyerek büyük, muhtemelen karmaşık bir veri yapısı oluşturuyorsanız bu harikadır. Farklı veri türlerine sahip olsanız bile, tüm nesneleriniz bellekte birbirine yakın olur ve bu da işlemcinin bu nesnelere hızlı erişmesine yardımcı olur. Ancak, zaman geçtikçe ve veri yapınız değiştirildiğinde, büyük olasılıkla yeni nesnelerin eski nesnelere eklenmesi gerekir. Bu yeni nesneler çok daha sonra oluşturulmuş olacak ve bu nedenle bellekteki özgün nesnelerin yakınında olmayacaktır. Çöp toplayıcı belleğinizi sıkıştırsa bile nesneleriniz bellekte karıştırılmaz, yalnızca boşa harcanan alanı kaldırmak için birlikte "kayar". Ortaya çıkan bozukluk zaman içinde o kadar kötü hale gelebilir ki, tüm veri yapınızın taze bir kopyasını, tamamen güzel bir şekilde paketlenmiş ve eski düzensiz olanın uygun bir zamanda toplayıcı tarafından kınanmasına izin verebilirsiniz.

Çok Fazla Kök

Çöp toplayıcı elbette toplama zamanında köklere özel işlem yapmalıdır; her zaman sırayla numaralandırılmalı ve düzgün bir şekilde göz önünde bulundurulmalıdır. 0. nesil koleksiyonu yalnızca dikkate alınması gereken bir kök seli vermediğiniz ölçüde hızlı olabilir. Yerel değişkenleri arasında çok sayıda nesne işaretçisi olan derin özyinelemeli bir işlev oluşturacaksanız, sonuç aslında oldukça maliyetli olabilir. Bu maliyet yalnızca tüm bu kökleri dikkate almak zorunda kalmakla kalmaz, aynı zamanda bu köklerin çok uzun süre canlı tutabileceği çok fazla sayıda0 . nesil nesnede de tahakkuk eder (aşağıda ele alınmalıdır).

Çok Fazla Nesne Yazma

Önceki tartışmamıza bir kez daha değinerek, yönetilen bir program bir nesne işaretçisini her değiştirişinde yazma engeli kodunun da tetiklendiğini unutmayın. Bu iki nedenden dolayı kötü olabilir:

İlk olarak, yazma engelinin maliyeti, en başta yapmaya çalıştığınız şeyin maliyetiyle karşılaştırılabilir. Örneğin, bir tür numaralandırıcı sınıfında basit işlemler yapıyorsanız, ana koleksiyondaki bazı anahtar işaretçilerinizi her adımda numaralandırıcıya taşımanız gerektiğini fark edebilirsiniz. Bu aslında kaçınmak isteyebileceğiniz bir şeydir çünkü yazma engeli nedeniyle bu işaretçileri kopyalama maliyetini ikiye katlarsınız ve bunu numaralandırıcıda döngü başına bir veya daha fazla kez yapmanız gerekebilir.

İkinci olarak, daha eski nesneler üzerinde yazıyorsanız yazma engellerini tetikleme iki kat kötüdür. Eski nesnelerinizi değiştirirken, bir sonraki çöp toplama işleminin ne zaman gerçekleştiğini denetlemek için etkili bir şekilde ek kökler oluşturursunuz (yukarıda açıklanmaktadır). Eski nesnelerinizi yeterince değiştirdiyseniz, yalnızca en genç nesli toplamayla ilişkili her zamanki hız iyileştirmelerini etkili bir şekilde olumsuzlarsınız.

Bu iki neden elbette herhangi bir programda çok fazla yazma yapmamanın normal nedenleriyle tamamlanmıştır. Her şey eşit olduğunda, işlemcinin önbelleğini daha ekonomik bir şekilde kullanmak için belleğinizin daha az bölümüne (okuma veya yazma) dokunmak daha iyidir.

Çok Fazla Neredeyse Uzun Ömürlü Nesne

Son olarak, belki de nesil çöp toplayıcısının en büyük tuzakları, ne geçici ne de tam olarak uzun ömürlü olan birçok nesnenin oluşturulmasıdır. Bu nesneler çok fazla sorun yaratabilir, çünkü bir0. nesil koleksiyonu (en ucuz) tarafından temizlenmeyeceklerdir, çünkü bunlar hala gerekli olacaktır ve hatta hala kullanımda oldukları için1. nesil bir koleksiyonda bile hayatta kalabilirler, ancak kısa süre sonra ölürler.

Sorun, bir nesne2 . nesil düzeyine ulaştığında, yalnızca tam bir toplamanın ondan kurtulması ve tam koleksiyonların çöp toplayıcının makul ölçüde mümkün olduğu sürece bunları geciktirmesi yeterince maliyetli olmasıdır. Bu nedenle birçok "neredeyse uzun ömürlü" nesneye sahip olmanın sonucu, 2 . nesilinizin potansiyel olarak endişe verici bir hızda büyüme eğiliminde olmasıdır; neredeyse istediğiniz kadar hızlı temizlenmeyebilir ve temizlendiğinde, bunu yapmak istediğinizden çok daha maliyetli olacaktır.

Bu tür nesneleri önlemek için en iyi savunma hattınız şöyle olur:

  1. Kullandığınız geçici alan miktarına dikkat edin, mümkün olduğunca az nesne ayırın.
  2. Daha uzun süreli nesne boyutlarını en düşük düzeyde tutun.
  3. Yığınınızda mümkün olduğunca az nesne işaretçisi tutun (bunlar köklerdir).

Bunları yaparsanız, 0 . nesil koleksiyonlarınızın yüksek oranda etkili olma olasılığı daha yüksektir ve1 . nesil çok hızlı büyümez. Sonuç olarak,1 . nesil koleksiyonları daha az sıklıkta yapılabilir ve1 . nesil koleksiyonu yapmak akıllıca olduğunda, orta ömürlü nesneleriniz zaten ölmüş olur ve o zaman ucuz bir şekilde kurtarılabilir.

İşler yolunda giderse, kararlı durum operasyonları sırasında2 . nesil boyutunuz hiç artmayacak!

Son -landırma

Basitleştirilmiş ayırma modeliyle ilgili birkaç konuyu ele aldığımıza göre, daha önemli bir olguyu daha tartışabilmemiz için işleri biraz karmaşık hale getirmek istiyorum ve bu da sonlandırıcıların ve sonlandırmanın maliyetidir. Kısaca, herhangi bir sınıfta bir sonlandırıcı bulunabilir; çöp toplayıcının söz konusu nesnenin belleğini geri kazanmadan önce aksi takdirde ölü nesneleri çağırmayı vaat eden isteğe bağlı bir üyedir. C# dilinde sonlandırıcıyı belirtmek için ~Class söz dizimini kullanırsınız.

Sonlandırma, Koleksiyonu Nasıl Etkiler?

Atık toplayıcı, aksi halde ölü olan ancak yine de sonlandırılması gereken bir nesneyle ilk kez karşılaştığında, o nesnenin alanını geri kazanma girişiminden o anda vazgeçmesi gerekir. Nesne bunun yerine sonlandırması gereken nesneler listesine eklenir ve ayrıca toplayıcının, sonlandırma tamamlanana kadar nesne içindeki tüm işaretçilerin geçerli kalmasını sağlaması gerekir. Bu temelde sonlandırmaya ihtiyaç duyan her nesnenin toplayıcı açısından geçici bir kök nesne gibi olduğunu söylemekle aynıdır.

Koleksiyon tamamlandıktan sonra, aptly adlı sonlandırma iş parçacığı , sonlandırması gereken nesne listesinden geçer ve sonlandırıcıları çağırır. Bu yapıldığında nesneler bir kez daha ölür ve doğal olarak normal şekilde toplanır.

Sonlandırma ve Performans

Bu temel sonlandırma anlayışıyla, çok önemli bazı şeyleri zaten çözebiliriz:

İlk olarak, sonlandırması gereken nesneler, olmayan nesnelerden daha uzun süre yaşar. Aslında, çok daha uzun yaşayabilirler. Örneğin,2 . nesildeki bir nesnenin sonlandırılması gerektiğini varsayalım. Sonlandırma zamanlanacak ancak nesne hala 2. nesilde olduğundan, bir sonraki2 . nesil koleksiyonu gerçekleşene kadar yeniden toplanmayacaktır. Bu gerçekten de çok uzun bir süre olabilir ve aslında işler yolunda giderse uzun bir süre olacaktır , çünkü2 . nesil koleksiyonlar maliyetlidir ve bu nedenle çok seyrek gerçekleşmesini istiyoruz . Sonlandırması gereken eski nesnelerin, alanları geri kazanılmadan önce yüzlerce0. nesil koleksiyon olmasa bile onlarca beklemesi gerekebilir.

İkincisi, kesinleştirilmesi gereken nesneler ikincil hasara neden olur. İç nesne işaretçilerinin geçerli kalması gerektiğinden, doğrudan sonlandırmaya ihtiyaç duyan nesneler bellekte kalır, aynı zamanda doğrudan ve dolaylı olarak nesnenin başvurduğu her şey de bellekte kalır. Büyük bir nesne ağacı, sonlandırma gerektiren tek bir nesne tarafından sabitlenmişse, daha önce konuştuğumuz gibi tüm ağaç uzun bir süre boyunca devam eder. Bu nedenle sonlandırıcıları tedbirli kullanmak ve mümkün olduğunca az iç nesne işaretçisine sahip nesnelere yerleştirmek önemlidir. Az önce verdiğim ağaç örneğinde, sonlandırılması gereken kaynakları ayrı bir nesneye taşıyarak ve bu nesneye başvuruyu ağacın kökünde tutarak sorundan kolayca kaçınabilirsiniz. Bu mütevazı değişiklikle yalnızca bir nesne (umarım küçük bir nesne) oyalanır ve sonlandırma maliyeti en aza indirilir.

Son olarak, sonlandırması gereken nesneler sonlandırıcı iş parçacığı için çalışma oluşturur. Sonlandırma işleminiz karmaşık bir işlemse, tek ve tek sonlandırıcı iş parçacığı bu adımları gerçekleştirmek için çok fazla zaman harcar ve bu da bir çalışma kapsamına neden olabilir ve bu nedenle sonlandırma için bekleyen daha fazla nesneye neden olabilir. Bu nedenle, sonlandırıcıların mümkün olduğunca az iş yapması hayati önem taşır. Ayrıca, sonlandırma sırasında tüm nesne işaretçilerinin geçerliliğini korusa da, bu işaretçilerin zaten sonlandırılmış nesnelere yol açması ve bu nedenle daha az yararlı olabileceğini unutmayın. İşaretçiler geçerli olsa bile sonlandırma kodunda nesne işaretçilerini takip etmekten kaçınmak genellikle en güvenlidir. Güvenli, kısa bir sonlandırma kodu yolu en iyisidir.

IDisposable ve Dispose

Çoğu durumda, IDisposable arabirimini uygulayarak bu maliyeti önlemek için her zaman sonlandırılması gereken nesneler mümkündür. Bu arabirim, yaşam süresi programcı tarafından iyi bilinen kaynakları geri kazanmak için alternatif bir yöntem sağlar ve bu aslında oldukça fazla olur. Tabii ki nesneleriniz yalnızca bellek kullanıyorsa ve bu nedenle sonlandırma veya hiç yok sayma gerektirmesi daha iyidir; ancak sonlandırma gerekliyse ve nesnelerinizin açık yönetiminin kolay ve pratik olduğu birçok durum varsa, IDisposable arabirimini uygulamak sonlandırma maliyetlerini önlemenin veya en azından azaltmanın harika bir yoludur.

C# ayrıştırmada bu desen oldukça yararlı olabilir:

class X:  IDisposable
{
   public X(…)
   {
   … initialize resources … 
   }

   ~X()
   {
   … release resources … 
   }

   public void Dispose()
   {
// this is the same as calling ~X()
        Finalize(); 

// no need to finalize later
System.GC.SuppressFinalize(this); 
   }
};

Dispose için el ile yapılan bir çağrı, toplayıcının nesneyi canlı tutması ve sonlandırıcıyı çağırması gereksinimini ortadan kaldırıyor.

Sonuç

.NET çöp toplayıcı, bellek kullanımı iyi olan ve uzun süreli parçalanma sorunu olmayan yüksek hızlı bir ayırma hizmeti sağlar, ancak size en iyi performanstan çok daha azını verecek şeyler yapmak mümkündür.

Ayırıcıdan en iyi şekilde yararlanmak için aşağıdaki gibi uygulamaları göz önünde bulundurmanız gerekir:

  • Belirli bir veri yapısıyla kullanılacak tüm belleği (veya mümkün olduğunca) aynı anda ayırın.
  • Karmaşıklıkta çok az cezayla önlenebilen geçici ayırmaları kaldırın.
  • Özellikle eski nesnelere yapılan yazma işlemleri olmak üzere nesne işaretçilerinin yazılma sayısını en aza indirin.
  • Veri yapılarınızdaki işaretçilerin yoğunluğunuzu azaltın.
  • Sonlandırıcıları sınırlı bir şekilde kullanın ve ardından mümkün olduğunca yalnızca "yaprak" nesneler üzerinde kullanın. Bu konuda yardımcı olmak için gerekirse nesneleri kırabilirsiniz.

Temel veri yapılarınızı gözden geçirme ve Ayırma Profili Oluşturucu gibi araçlarla bellek kullanım profillerini yürütmeye yönelik düzenli bir uygulama, bellek kullanımınızı etkili tutmanın ve çöp toplayıcının sizin için en iyi şekilde çalışmasını sağlamanın uzun bir yolu olacaktır.