Eden Space
Benim sorum şu, bunların herhangi biri gerçekten doğru olabilir mi ve eğer öyleyse java'nın yığın dağılımı neden bu kadar hızlı?
Benim için çok ilginç olduğu için Java GC'nin nasıl çalıştığı hakkında biraz çalışıyorum. Her zaman C ve C ++ bellek ayırma stratejileri koleksiyonumu genişletmeye çalışıyorum (C'de benzer bir şey uygulamaya çalışmakla ilgileniyorum) ve çok sayıda nesneyi patlama biçiminde ayırmanın çok, çok hızlı bir yoludur. pratik bakış açısı, ancak esas olarak çoklu iş parçacığı nedeniyle.
Java GC tahsisinin çalışma şekli, nesneleri başlangıçta "Eden" alanına tahsis etmek için son derece ucuz bir tahsis stratejisi kullanmaktır. Söyleyebileceğim kadarıyla, sıralı bir havuz ayırıcı kullanıyor.
Bu sadece algoritma ve genel amaçlı daha zorunlu sayfa hataları azaltmak açısından bir sürü daha hızlı malloc
atma, C veya varsayılan operator new
C ++.
Ancak sıralı ayırıcıların göze çarpan bir zayıflığı vardır: değişken boyutlu parçalar tahsis edebilirler, ancak tek tek parçaları serbest bırakamazlar. Sadece hizalama için dolgu ile düz sıralı bir şekilde tahsis ederler ve sadece bir kerede tahsis ettikleri tüm belleği temizleyebilirler. Genellikle C ve C ++ 'da, yalnızca bir program başlatıldığında ve daha sonra tekrar arandığında veya yalnızca yeni anahtarlar eklendikten sonra yalnızca bir kez oluşturulması gereken bir arama ağacı gibi, öğelerin yalnızca eklenmesi ve kaldırılması gerekmeyen veri yapılarını oluşturmak için kullanışlıdırlar ( anahtar kaldırılmadı).
Ayrıca, öğelerin kaldırılmasına izin veren veri yapıları için bile kullanılabilirler, ancak bu öğeler gerçekte bellekten kurtarılmayacaktır, çünkü bunları ayrı ayrı ayıramayız. Sıralı bir ayırıcı kullanan böyle bir yapı , verilerin ayrı bir sıralı ayırıcı kullanılarak taze, sıkıştırılmış bir kopyaya kopyalandığı bazı ertelenmiş geçişleri olmadıkça (ve sabit bir ayırıcı kazandığında bazen çok etkili bir teknik olmadıkça) daha fazla bellek tüketirdi. nedense yapmayın - sadece sırayla veri yapısının yeni bir kopyasını ayırın ve eskisinin hafızasını boşaltın).
Toplamak
Yukarıdaki veri yapısı / sıralı havuz örneğinde olduğu gibi, Java GC'nin birçok ayrı parçanın çoğaltılması için süper hızlı olmasına rağmen sadece bu şekilde ayrılması büyük bir sorun olacaktır. Yazılım kapatılana kadar hiçbir şey boşaltamaz, bu noktada tüm bellek havuzlarını aynı anda serbest bırakabilir (temizleyebilir).
Bu nedenle, bunun yerine, tek bir GC döngüsünden sonra, "Eden" uzayındaki (ardışık olarak tahsis edilen) mevcut nesnelerden bir geçiş yapılır ve daha sonra referansta bulunulanlar, ayrı ayrı parçaları serbest bırakabilen daha genel amaçlı bir ayırıcı kullanılarak tahsis edilir. Artık başvurulmayanlar tasfiye sürecinde kolayca yer değiştirecektir. Yani, temelde "hala referans alınıyorsa nesneleri Eden alanından kopyalayıp temizler".
Bu normalde oldukça pahalıdır, bu nedenle başlangıçta tüm belleği tahsis eden ipliğin önemli ölçüde durmasını önlemek için ayrı bir arka plan iş parçacığında yapılır.
Bellek Eden alanından kopyalandıktan ve ilk GC döngüsünden sonra ayrı ayrı parçaları serbest bırakabilen bu daha pahalı şema kullanılarak ayrıldıktan sonra, nesneler daha kalıcı bir bellek bölgesine taşınır. Bu münferit parçalar daha sonra referans gösterilmemesi durumunda sonraki GC döngülerinde serbest bırakılır.
hız
Bu nedenle, Java GC'nin düz yığın tahsisinde C veya C ++ 'dan çok daha iyi performans göstermesinin nedeni, bellek ayırmak isteyen iş parçacığında en ucuz, tamamen dejenere edilmiş tahsis stratejisini kullanmasıdır. Daha sonra malloc
, başka bir iş parçacığı için düzleştirme gibi daha genel bir ayırıcı kullanırken normalde yapmamız gereken daha pahalı işi kaydeder .
Dolayısıyla kavramsal olarak GC aslında daha fazla iş yapmak zorundadır, ancak tam maliyetin tek bir iş parçacığı tarafından önceden ödenmemesi için bunu iş parçacıkları arasında dağıtır. Bellek ayırma iş parçacığının süper ucuz yapmasına izin verir ve daha sonra işleri düzgün yapmak için gereken gerçek masrafı erteleyerek tek tek nesnelerin aslında başka bir iş parçacığına serbest kalmasını sağlar. C veya C ++ 'da biz malloc
veya aradığımızda operator new
, tam maliyetini aynı iş parçacığı içinde önceden ödemek zorundayız.
Bu ana farktır ve Java neden sadece saf çağrılar kullanarak malloc
veya operator new
bir grup ufacık parçaya ayrı ayrı tahsis ederek C veya C ++ 'dan çok iyi performans gösterebilir . Tabii ki, GC döngüsü devreye girdiğinde tipik olarak bazı atom işlemleri ve bazı potansiyel kilitleme olacaktır, ancak muhtemelen biraz optimize edilmiştir.
Temel olarak basit açıklama, tek bir iş parçacığında malloc
daha pahalı bir maliyet ödemekle ( ) tek bir iş parçacığında daha ucuz bir maliyet ödemek ve daha sonra paralel olarak çalışabilen başka bir işyerinde daha ağır maliyet ödemek anlamına gelir GC
. İşleri bu şekilde yapmanın bir dezavantajı olarak, ayırıcının mevcut nesne referanslarını geçersiz kılmadan belleği kopyalamasına / hareket etmesine izin vermek için gereken şekilde nesne referansından nesneye almak için iki dolaylamaya ihtiyaç duyduğunuz anlamına gelir ve ayrıca nesne hafızası olduğunda uzamsal konumu kaybedebilirsiniz "Eden" alanından taşındı.
Son olarak, C ++ kodu normalde yığın üzerinde tek tek bir nesne yükü ayırmadığından karşılaştırma biraz haksızdır. İyi C ++ kodu, bitişik bloklardaki veya yığındaki birçok öğe için bellek ayırma eğilimindedir. Ücretsiz mağazada birer birer küçük nesneler yükü tahsis ederse, kod boktan olur.