Monday, September 15, 2014

Java 8'de String Deduplication


Oracle'ın Java 8'i Mart 2014'de duyurmasından tam 6 ay geçti. Şu an JDK'nın en güncel sürümü 8u20. Son sürümü bu bağlantıdan indirip deneyebilirsiniz. Artık platform olgunlaşmaya başladı. JDK 8u20 ile gelen birkaç ilginç yenilik de bulunuyor. Bu özelliklerden biriyle G1 çöp toplayıcısının kullanılması durumunda karşılaşıyoruz. Garbage First (G1), çöp toplama zamanı elvermesi halinde, karakter katarlarını sabit havuzuna alıyor (String Deduplication). Sabit havuzu eskiden PermGen olarak adlandırılan alanda yer alıyordu. Artık Java 8'de PermGen yok, yerine MetaSpace olarak adlandırılan bellek bölgesi geldi. MetaSpace alanının eski PermGen'e göre en önemli kazanımı, dinamik olarak genişleyebiliyor olmasıdır. Sabit havuzunu derleme aşamasında değeri belirli String, bir sekizliğe sığan ([-128..127])Integer gibi tamsayı sınıfları kullanır:
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class TestConstantPool {
    public static void main(String[] args) {
        String s1= "Hello"; // CP
        String s2= "Hello"; // CP
        // String is immutable
        String s3= new String("Hello"); // Heap (Eden)
        String s4= new String("Hello"); // Heap (Eden)
        if (s1==s2){
            System.out.println("s1 is equal to s2");
        }else{
            System.out.println("s1 is NOT equal to s2");
        }
        if (s4==s3){
            System.out.println("s1 is equal to s3");
        }else{
            System.out.println("s1 is NOT equal to s3");
        }
        s3= s3.intern();
        if (s1==s3){
            System.out.println("s1 is equal to s3");
        }else{
            System.out.println("s1 is NOT equal to s3");
        }       
    }
}
Burada s1 ve s2 sabit havuzunda yaratılan aynı karakter katarını ("Hello") gösterirken s3 ve s4 Heap'de, içeriği aynı olsa da farklı bellek adreslerinde yaratılan nesneleri gösterir. Bu yüzden 8. satırdaki karşılaştırma doğru, 13. satırdaki karşılaştırma yanlıştır. String sınıfında intern() isimli bir çağrı yer alır. Bu çağrıyı kullanarak bir String nesnesini sabit havuzuna almak mümkündür. 18. satırdaki s3= s3.intern() çağrısı ile artık s3 sabit havuzundaki "Hello" katarını göstermektedir. Böylelikle s1, s2 ve s3 artık aynı nesneyi gösterir. Bu yüzden 19. satırdaki s1==s3 karşılaştırması doğrudur.
Aşağıdaki kodda ise 108 bir sekizliğe sığdığı için a ve b sabit havuzunda, 549 ise sekizliğe sığmadığı için m ve n ise Heap'deki nesneleri gösterir:
public class TestInteger {
    public static void main(String[] args) {
        Integer a= 108; // CP
        Integer b= 108; // CP
        Integer m= 549; // Heap (Eden)
        Integer n= 549; // Heap (Eden)
        if (a==b){
            System.out.println("a is equal to b");
        } else {
            System.out.println("a is NoT equal to b");
        }
        if (m==n)){
            System.out.println("m is equal to n");
        } else {
            System.out.println("m is NoT equal to n");
        }
    }
}
Bu yüzden a==b karşılaştırması doğru iken m==n karşılaştırması yanlıştır. 
"String deduplication" özelliği normalde kapalı geliyor. Bu özelliği açmak için Hotspot'u -XX:+UseG1GC -XX:+UseStringDeduplication seçenekleri ile başlatmalısınız. Aradaki farkı izlemek için aşağıdaki program parçasını kullanalım. 
public class LotsOfStrings {
 
  private static final LinkedList<String> LOTS_OF_STRINGS = new LinkedList<>();
 
  public static void main(String[] args) throws Exception {
    int iteration = 0;
    while (true) {
      for (int i = 0; i < 100; i++) {
        for (int j = 0; j < 1000; j++) {
          LOTS_OF_STRINGS.add(new String("String " + j));
        }
      }
      iteration++;
      System.out.println("Survived Iteration: " + iteration);
      Thread.sleep(100);
    }
  }
}
Kodu dikkatlice incelediğimizde sonsuz döngü içinde sürekli String nesnesi yaratıldığını görüyoruz. Kodu çalıştırdığımızda kaçınılmaz olarak Yetersiz Bellek Hatası (java.lang.OutOfMemoryError) verecektir. 128MB kapasiteli bir heap için bakalım kaç döngü dayanabilecek: -Xmx128m -Xms128m
Survived Iteration: 1
Survived Iteration: 2
Survived Iteration: 3
Survived Iteration: 4
Survived Iteration: 5
Survived Iteration: 6
Survived Iteration: 7
Survived Iteration: 8
Survived Iteration: 9
Survived Iteration: 10
Survived Iteration: 11
Survived Iteration: 12
Survived Iteration: 13
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
 at com.example.console.LotsOfStrings.main(LotsOfStrings.java:18)
Şimdi tekrar bu özelliği açarak yeniden çalıştıralım ve aradaki farkı görmeye çalışalım: 
-Xms128m -Xmx128m -XX:+UseG1GC -XX:+UseStringDeduplication
Survived Iteration: 1
Survived Iteration: 2
Survived Iteration: 3
Survived Iteration: 4
Survived Iteration: 5
Survived Iteration: 6
Survived Iteration: 7
Survived Iteration: 8
Survived Iteration: 9
Survived Iteration: 10
Survived Iteration: 11
Survived Iteration: 12
Survived Iteration: 13
Survived Iteration: 14
Survived Iteration: 15
Survived Iteration: 16
Survived Iteration: 17
Survived Iteration: 18
Survived Iteration: 19
Survived Iteration: 20
Survived Iteration: 21
Survived Iteration: 22
Survived Iteration: 23
Survived Iteration: 24
Survived Iteration: 25

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
Java Result: 1
Şimdi döngü 25 defa dönebildi ve ardından Yetersiz Bellek Hatası (java.lang.OutOfMemoryError) aldık. Peki bu özelliği açmak bize ne kazandıracak? Bu soruyu cevaplamak için aşağıdaki istatistiğe bakmak yeterli olur:
  • Heap'deki canlı nesnelerin ortalama dörtte biri String nesnesidir.
  • Heap'de yinelenen String oranı ortalama %13.5'dir.
  • Ortalama String uzunluğu 45'dir.
Heap üzerinde ortalama olarak %5-10'luk bir rahatlama getirmesi beklenmelidir. Buna karşılık olarak yinelenen String nesnelerin sabit havuzuna atmak için G1 çöp toplayıcısının işlemci zamanımızdan çaldığını ve bunun bir maliyeti olduğunu unutmayalım. Son bir nokta, G1 çöp toplayıcı eğer vakit kalırsa bu işlemi yaptığını ve kodda çöp toplayıcıya vakit tanımak için uygulamanın uykuya daldığını (Thread.sleep(100)) hatırlatırım. Thread.sleep(100) satırını koddan çıkarıp, tekrar çalıştırırsak uygulamanın tepkisi aşağıdaki gibi olmaktadır:
Survived Iteration: 1
Survived Iteration: 2
Survived Iteration: 3
Survived Iteration: 4
Survived Iteration: 5
Survived Iteration: 6
Survived Iteration: 7
Survived Iteration: 8
Survived Iteration: 9
Survived Iteration: 10
Survived Iteration: 11
Survived Iteration: 12
Survived Iteration: 13
Survived Iteration: 14
Survived Iteration: 15
Survived Iteration: 16
Survived Iteration: 17
Survived Iteration: 18
Survived Iteration: 19

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
Java Result: 1 
Deduplication yükünü kontrol etmek için kullanabileceğimiz bir parametremiz var: -XX:StringDeduplicationAgeThreshold. Değerini artırarak bu en iyilemenin genç String nesneleri yerine, yaşı geçkince olan String’lere uygulanması sağlanabilir. Varsayılan değeri 3’dür.
Bu özelliği açmanın gerçekten işe yarayıp yaramadığına karar vermek için yapılan işlemin başarımını kayıtlardan izlemek gerekir. Bunun için -XX:+PrintStringDeduplicationStatistics seçeneğini vermek yeterli olur:
Survived Iteration: 1
[GC concurrent-string-deduplication, 2131.9K->39.3K(2092.6K), avg 98.2%, 0.0121525 secs]
   [Last Exec: 0.0121525 secs, Idle: 0.0000008 secs, Blocked: 0/0.0000000 secs]
      [Inspected:           54680]
         [Skipped:              0(  0.0%)]
         [Hashed:           54680(100.0%)]
         [Known:                0(  0.0%)]
         [New:              54680(100.0%)   2131.9K]
      [Deduplicated:        53678( 98.2%)   2092.6K( 98.2%)]
         [Young:                0(  0.0%)      0.0B(  0.0%)]
         [Old:              53678(100.0%)   2092.6K(100.0%)]
   [Total Exec: 1/0.0121525 secs, Idle: 1/0.0000008 secs, Blocked: 0/0.0000000 secs]
      [Inspected:           54680]
         [Skipped:              0(  0.0%)]
         [Hashed:           54680(100.0%)]
         [Known:                0(  0.0%)]
         [New:              54680(100.0%)   2131.9K]
      [Deduplicated:        53678( 98.2%)   2092.6K( 98.2%)]
         [Young:                0(  0.0%)      0.0B(  0.0%)]
         [Old:              53678(100.0%)   2092.6K(100.0%)]
   [Table]
      [Memory Usage: 49.5K]
      [Size: 1024, Min: 1024, Max: 16777216]
      [Entries: 1770, Load: 172.9%, Cached: 0, Added: 1770, Removed: 0]
      [Resize Count: 0, Shrink Threshold: 682(66.7%), Grow Threshold: 2048(200.0%)]
      [Rehash Count: 0, Rehash Threshold: 120, Hash Seed: 0x0]
      [Age Threshold: 3]
   [Queue]
      [Dropped: 0]
Burada ilk bakılması gereken satır özetçenin yer aldığı ilk satırdır:
[GC concurrent-string-deduplication, 2131.9K->39.3K(2092.6K), avg 98.2%, 0.0121525 secs]
Bu satırdan, String deduplication için ne kadar süre harcandığı ve ortalama ne kadar yineleme yakalandığını okumak mümkündür. Karar sizin.
View Binnur Kurt's profile on LinkedIn

No comments:

Post a Comment