Sorunun geçmişi
Java 8 güncellemesi 20'den önce, çift String örneklerinden yığın tüketimini azaltmak isteyen geliştiriciler yalnızca String.intern() yöntemine güvenmek zorunda kalıyordu. Bu yöntem, dizeleri kalıcı nesil (daha sonra Metaspace) içine koyarak açık API çağrıları gerektiriyor ve intern havuzunda bellek baskısı yaratma potansiyeline sahipti. JEP 192 ile, G1 çöp toplayıcı, gereksiz karakter dizileri sorununu hedefleyen otomatik String Deduplication (Dizelerin Deduplicasyonu) özelliğini tanıttı.
Problem
Veri yoğun Java uygulamalarında — XML, JSON ya da veri tabanı sonuç kümelerini ayrıştıran uygulamalar gibi — String nesneleri genellikle canlı yığının %25-50'sini oluşturur. Bu dizelerin önemli bir kısmı karakter bazında identiktir ancak farklı char[] (ya da Java 9 sonrası Compact Strings için byte[]) destek dizilerinde yer alır. Müdahale olmadan, bu kopya diziler bellek israfına neden olur ve GC sıklığını artırır. Sorun, bu fazlalıkları, ek durma süreleri ya da kod değişiklikleri gerektirmeden ortadan kaldırmaktı.
Çözüm
G1, mevcut tahliye duraklama süresi sırasında, duraklamış olan iş parçacıkları varken fırsatçı bir şekilde deduplike işlemi gerçekleştirir. -XX:+UseStringDeduplication ile etkinleştirildiğinde, toplayıcı genç kuşaktaki nesneleri tarar. En az -XX:StringDeduplicationAgeThreshold çöp toplama döngüsünü (varsayılan 3) geçirmiş her String için, G1 destek dizisinin bir hash'ini hesaplar. Ardından bir deduplike tablosuna danışır. Eğer aynı dizi varsa, G1, compare-and-swap (CAS) işlemini kullanarak String'in value alanını mevcut diziye yönlendirir ve kopyayı bir sonraki döngüde geri kazandırır. Bu, mevcut duraklamadan yararlanır, yalnızca marjinal bir CPU yükü ekler.
// Hiçbir kod değişikliği gerektirmez; JVM bayrakları optimizasyonu etkinleştirir: // -XX:+UseG1GC -XX:+UseStringDeduplication -XX:StringDeduplicationAgeThreshold=3 public class DeduplicationExample { public static void main(String[] args) { // Bu iki dize, deduplike işlemi sonrasında aynı destek dizisini paylaşır String a = new String("FinancialInstrument".toCharArray()); String b = new String("FinancialInstrument".toCharArray()); // Yeterli çöp toplama döngüsü ve tahliye duraklamasından sonra, // a.value == b.value (içsel dizi referans eşitliği) } }
FIX protokol mesajlarını işleyen bir yüksek frekanslı ticaret platformu, 200ms'yi aşan ciddi G1 duraklama süreleri yaşadı. Profilleme, 64GB yığının %30'unun standart etiketleri temsil eden String nesneleri (örneğin "55", "150", "EUR/USD") tarafından tüketildiğini ortaya koydu. Her mesaj örneği, yeni String örnekleri oluşturarak new String(byte[], Charset) ile sonuçlandı, bu da dakikada milyonlarca kopya destek dizisi yarattı.
Birçok çözüm değerlendirildi. String.intern() yöntemi, 50'den fazla mesaj türünde müdahaleci değişiklikler gerektirdiği ve kalıcı referanslar ile Metaspace'i doydurma riski taşıdığı için reddedildi. Özel bir WeakHashMap tabanlı önbellek prototipi oluşturuldu, ancak karmaşık eşzamanlılık yükü ve eskimiş giriş temizleme mantığı ekleyerek, ek WeakReference işlemesi nedeniyle GC baskısını artırdı.
Takım sonunda varsayılan yaş eşiği 3 olan G1 String Deduplication'u etkinleştirdi. Bu şeffaf yaklaşım hiç kod değişikliği gerektirmedi ve mevcut tahliye duraklamaları sırasında çalışarak yeni durma sürelerini önledi.
Sonuç, yığın kullanımında %22'lik bir azalma ve 95. percentil duraklama sürelerinin 50ms'nin altına düşmesi oldu. Zirve piyasa saatlerinde ölçülen CPU yükü yaklaşık %1.5 oldu, bu da bellek tasarrufu ve gecikme iyileştirmesi için kabul edilebilir bir dengeydi.
String deduplike işlemi, Latin-1 metni byte[] olarak saklayan Java 9'un Compact Strings ile nasıl etkileşir?
Cevap. String Deduplication, Compact Strings etkinleştirildiğinde byte[] dizileri üzerinde çalışacak şekilde güncellendi (Java 9'dan beri varsayılan). Deduplication mantığı coder alanını (LATIN1 veya UTF16) inceler ve ilgili byte[] ya da char[] destek dizisini ona göre hashler. Deduplikasyon tablosu, hem hash hem de dizi türüne göre anahtarlanan girişleri saklar, Latin-1 dizeleri diğer Latin-1 dizelerine karşı ve tam boy UTF-16 dizeleri kendi akranlarına karşı deduplike edilir. Adaylar genellikle bu özelliğin Compact Strings ile iptal edildiğini yanlış anlar, ancak tamamen uyumlu kalmaya devam eder.
JVM neden bir String'in deduplike edilmeden önce bir yaş eşiği (varsayılan 3 GC) koyar?
Cevap. Yaş eşiği, sistemi, büyük ihtimalle bir sonraki genç koleksiyonda kaybolacak kısa ömürlü, geçici dizeleri deduplike ederek CPU döngülerini boşa harcamaktan alıkoyar. String'in birkaç G1 tahliye döngüsünü (Eden'dan Survivor bölgelerine ve sonunda Tenured'a yükselterek) geçirmesi istenmesi, yalnızca "olgun" dizelerin — uzun vadeli hayatta kalma olasılığı yüksek olanların — işlenmesini garanti eder. Bu, hash hesaplama ve tablo sorgulama maliyetini nesnenin beklenen ömrüne yayar.
String deduplication, String örneğinin değişmezliğini veya hashCode kararlılığını etkiler mi?
Cevap. Hayır. Deduplication işlemi, yalnızca value alanı referans mutasyonunun bir uygulama detayıdır. Değiştirilen dizi eşit byte veya karakter içerdiğinden, String'in mantıksal durumu ve hashCode değişmeden kalır. hashCode, String nesnesinin içinde geçici bir alanda cache'lenir ve içerik aynı olduğundan, cache'lenmiş değer geçerliliğini korur. equals sözleşmesi, içerik eşitliği, destek depolarının referans eşitliğinin API sözleşmesine kayıtsız olduğunu gösterdiği için korunur. İşlem, uygulama perspektifinden atomiktir ve String'in değişmez garantisini sürdürür.