Bir Java uygulaması için Java sanal makinesi (JVM) tarafından oluşturulan ve yönetilen hafıza alanının (heap) ortalama %25’ini String nesneleri kaplar. Bir heapdump oluşturduğumuzda, String nesneleri ve String nesnelerini oluşturan char[] arraylerin ilk sıralarda olduğunu görebiliriz. Şu şekilde örneğin çalışan bir Java uygulamasının hafıza resmi alınabilir.
[sourcecode language=”java”]
jmap -dump:live,format=b,file=<filename> <PID>
[/sourcecode]
Eclipse MAT ile heapdump çıktısını inceledigimizde, şu şekildeki bir resimle karşılaşmamız muhtemel:
Bu örnekte hafıza alanının yaklaşık olarak %25’inin String nesneleri ve String sınıfında yer alan char[] array nesneleri tarafından kullanıldığını görmekteyiz. Uygulamanın yapısına göre bu değer %40’lara kadar çıkabilir.
Java 8, 20 update sürümü ile String nesnesi kullanım oranlarını aşağıya çekmek mümkün. Bu sürüm ile G1 garbage collector tarafından kullanıma sunulan String deduplication mekanizmasından faydalanarak, String nesne kullanım oranını düşünebiliriz.
String deduplication ile aynı yapıda olan (s1.equals(s2) == true) birden fazla String nesnesi aynı char[] array nesnesini ortak kullanacak şekilde garbage collector tarafından yeniden yapılandırılmaktadırlar. String sınıfının aşağıdaki şekilde iki değişkeni bulunmaktadır:
[sourcecode language=”java”]
private final char value[];
private int hash; // Default to 0
[/sourcecode]
Bir String nesnesini oluşturan harfler value[] array nesnesi içinde yer alırlar. Her harf için 2 byte değerinde hafıza alanına ihtiyaç duyulmaktadır. Dışarıdan value[] nesnesine erişmek mümkün değildir. Bu yüzden String nesneleri değiştirilemez. Lakin String sınıfına baktığımızda, sınıf bünyesinde de value[] arrayi üzerinde işlem yapılmadığını görmekteyiz, yani value[] arrayi ne içten, ne de dıştan değişikliğe uğramaktadır. Bu durumda birden fazla String nesnesini aynı value[] array nesnesini kullanacak şekilde yeniden yapılandırmak mümkündür. Örneğin aynı içeriğe sahip olan on değişik String nesnesi sadece bir char[] array nesnesine işaret edecektir. Bu hafızada on yerine sadece bir adet char[] array nesnesi oluşturulması anlamına gelmektedir. Bu şekilde hafıza alanından tasarruf yapılabilir.
String deduplication G1 garbage collector tarafından yapılabilen bir işlemdir. Programcı olarak dışarıdan bu mekanizmaya müdahil olmamız mümkün değil. Sadece gerekli JVM ayarlarını yaparak, bu işlemi başlatabiliriz. G1 garbage collector String nesnelerinin hash değerlerini oluşturur ve zayıf bir referans (weak reference) ile String nesnesi ile char[] array arasındaki ilişkiyi yeniden yapılandırır. G1 hafıza alanını temizlerken String nesnelerinin hash değerlerini kıyaslar. Eğer iki String nesnesinin hash değerleri aynı ise, aynı içeriğe sahip olma ihtimalleri yüksektir. Bu durumda G1 iki nesnenin value[] array içeriklerini kıyaslar. Eğer iki value[] array aynı değere sahipse, G1 elinde tuttuğu String nesnesinin value[] arrayini kıyasladığı value[] arrayi gösterecek şekilde yeniden yapılandırır. Bu durumda ilk String nesnesinin value[] nesnesi boşta kalır. Zayıf bir referansa sahip olan bu nesne bir sonraki garbage collection işleminde hafızadan silinir.
Şimdi bu mekanizmanın nasıl işlediğini bir örnek üzerinde inceleyelim.
[sourcecode language=”java”]
package com.pratikprogramci.string;
import java.util.LinkedList;
public class StringDeduplicationTest {
private static final LinkedList<String> STRING_LIST =
new LinkedList<>();
public static void main(String[] args) throws Exception {
int iteration = 0;
while (true) {
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10000; j++) {
STRING_LIST.add(new String("Test String " + j));
}
}
iteration++;
System.out.println("Iterasyon sayisi: " + iteration);
Thread.sleep(100);
}
}
}
[/sourcecode]
Yukarıda yer alan kodu -Xmx128m -XX:+UseG1GC JVM parametreleri ile koşturduğumuzda, şu ekran çıktısını alırız:
[sourcecode language=”java”]
Iterasyon sayisi: 1
Iterasyon sayisi: 2
Iterasyon sayisi: 3
Iterasyon sayisi: 4
Iterasyon sayisi: 5
Iterasyon sayisi: 6
Iterasyon sayisi: 7
Iterasyon sayisi: 8
Iterasyon sayisi: 9
Iterasyon sayisi: 10
Iterasyon sayisi: 11
Iterasyon sayisi: 12
Iterasyon sayisi: 13
Exception: java.lang.OutOfMemoryError thrown from the
UncaughtExceptionHandler in thread "main"
[/sourcecode]
Uygulamayı 128 MB hafıza alanı ile başlattık. Sonsuz bir döngü içinde binlerce String nesnesi oluşturuyoruz. Uygulamamız 13 iterasyondan sonra OutOfMemoryError hatası ile son buldu, cünkü kullanabileceğimiz hafıza alanı kalmadı.
Şimdi uygulamayı String deduplication mekanizması ile çalıştıralım. Aşağıdaki JVM parametreleri kullanıyoruz:
[sourcecode language=”java”]
-Xmx128m -XX:+UseG1GC -XX:+UseStringDeduplication
-XX:+PrintStringDeduplicationStatistics
[/sourcecode]
Uygulamamızı tekrar çalıştırdığımızda, şöyle bir ekran çıktısı alırız:
[sourcecode language=”java”]
Iterasyon sayisi: 24
[GC concurrent-string-deduplication, 1998,8K->0,0B(1998,8K), avg 99,5%, 0,0101200 secs]
[Last Exec: 0,0101200 secs, Idle: 0,1232091 secs, Blocked: 3/0,5220587 secs]
[Inspected: 42641]
[Skipped: 0( 0,0%)]
[Hashed: 42641(100,0%)]
[Known: 0( 0,0%)]
[New: 42641(100,0%) 1998,8K]
[Deduplicated: 42641(100,0%) 1998,8K(100,0%)]
[Young: 0( 0,0%) 0,0B( 0,0%)]
[Old: 42641(100,0%) 1998,8K(100,0%)]
[Total Exec: 17/0,7226112 secs, Idle: 17/2,6424357 secs, Blocked: 16/1,4228223 secs]
[Inspected: 2112793]
[Skipped: 0( 0,0%)]
[Hashed: 2111921(100,0%)]
[Known: 767( 0,0%)]
[New: 2112026(100,0%) 96,7M]
[Deduplicated: 2101850( 99,5%) 96,2M( 99,5%)]
[Young: 21( 0,0%) 1008,0B( 0,0%)]
[Old: 2101829(100,0%) 96,2M(100,0%)]
[Table]
[Memory Usage: 320,5K]
[Size: 8192, Min: 1024, Max: 16777216]
[Entries: 10803, Load: 131,9%, Cached: 140, Added: 10943, Removed: 140]
[Resize Count: 3, Shrink Threshold: 5461(66,7%), Grow Threshold: 16384(200,0%)]
[Rehash Count: 0, Rehash Threshold: 120, Hash Seed: 0x0]
[Age Threshold: 3]
[Queue]
[Dropped: 0]
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
[/sourcecode]
İterasyon sayısının 13’den 24’e yükseldiğini görmekteyiz. Aynı şartlar altında uygulamamız daha uzun soluklu çalıştı. Bunun yanı sıra -XX:+PrintStringDeduplicationStatistics parametresi ile deduplication istatistiklerini görmekteyiz. Son iterasyonda G1 42641 adet String nesnesini 0,0101200 saniyede analiz etti. Bu analiz sonucunda 1998,8 KB hafıza alanını tekrar kullanılmak üzere temizlendi. Lakin bu uygulamanın OutOfMemoryError ile son bulması için yeterli olmadı.
EOF (End Of Fun)
Özcan Acar
Yorumlar
“Java String Nesnelerinin Hafıza Kullanımı Nasıl Azaltılır?” için 5 yanıt
Yazıların altında sosyal medya ikonları dışında like/beğen butonu da olmalı 🙂
bu parametrenin performansa etkisi nedir ? string duplikasyonlarını bulmasıda ayrı bir cpu time almaz mı ?
Garbage collection süresini uzatabilir. Lakin parallel garbage collection yapildigi icin uygulama bünyesinde olumsuz performans etkisi hissedilmeyecektir.
G1 çöp toplayıcı eğer süresi varsa bu işlemi yapar. Sistem yüklü ise bu en iyilemeyi çalıştırmaz. Koddaki Thread.sleep(100); çağrısının amacı da budur, uygulamanın yükünü azaltmak ve G1’e deduplication için fırsat yaratmak. G1 genç String’lere “deduplication” işlemini uygulamaz. Dedublication yükünü azaltmak için kullanabileceğimiz bir parametre de var: -XX:StringDeduplicationAgeThreshold. Değerini artırarak bu en iyilemeyi ancak yaşı geçkince olan String’lere uygulanması sağlanabilir. Varsayılan değeri 3’dür.
Hocam sanırım bu işlemi reflection ile de yapabiliriz diye düşünüyorum.