String Deduplication zum Sparen von Speicherplatz in Java 8

Strings nehmen in den meisten Anwendungen den Großteil des Speicherplatzes ein. Wobei wir es hierbei nicht selten mit Duplikaten zu tun haben.
Häufiger Verursacher ist hierbei das Laden von Objekten aus externen Quellen wie Datenbanken oder das Parsen von externen String basierten Requests. Werden Entitäten in großen Mengen (z.B. Adressen) geladen,

public class Address {
   private String strasse;
   private String plz;
   private String ort;
   ...
}

so wird für jede Entitäten-Instanz Speicherplatz für die entsprechenden String-Instanz-Variablen reserviert. Am Beispiel von Adressen können wir uns gut vorstellen, dass hierbei die gleichen Ortsbezeichnungen wie “Hamburg” häufig in mehreren Adressinstanzen vorkommen und jeweils Speicherplatz zur Ablage des char-Arrays benötigt. Bei großen Datenverarbeitungen wird entsprechend viel Speicherplatz verwendet. Auch viele Leer-String Objekte können beobachtet werden.

Schon länger existiert für dieses Problem die Lösung des String Internings. Die Methode intern() auf einem Stringobjekt bewirkt, dass die Zeichenkette gegen eine interne StringTable verglichen wird. Existiert die Zeichenabfolge bereits in der Table, so wird eine Reference auf die bereits bestehende zurückgeliefert oder aber neu in der StringTable angelegt.
Dies führt dazu, dass gleiche Zeichenfolgen nur einmal im Speicher gehalten werden. Ein deutlich kleinerer Memory Footprint ist die Folge. Die StringTable ist hierbei im PermanentSpace des Java Speichers abgelegt. String Konstanten im Quellcode werden deswegen schon länger per Default mittels String Interning abgelegt.

Seit Java 8 update 20 steht nun in der Oracle JVM die Option -XX:+UseStringDeduplication zur Verfügung, welche sich automatisch um die Beseitigung von duplizierten Strings bemüht. Die Option ist per Default zurzeit noch nicht aktiviert und muss daher aktiviert werden. Dies rührt auch daher, weil das Feature den G1 Garbage Collector verwendet und dieser dementsprechend auch aktiviert sein muss (Option -XX:+UseG1GC in der Oracle JVM).

Der Garbage Collector prüft bei der Abarbeitung wenn er auf einen String stößt, ob dessen Zeichenkette bereits als Duplikat bekannt ist. Hierbei ist die Zeichenkette, also das interne char[] gemeint. Durch einen Hashwert über das char[] und, im Fall eines Matches, darauffolgend ein exakter Vergleich der Zeichen. Wird auf diese Weise ein Duplikat gefunden, so wird das char[] direkt im entsprechenden String Objekt ausgetauscht. Die Referenzen auf den eigentlichen String bleiben für die Anwendung transparent erhalten.

Interessant ist hierbei auch die Tatsache, dass die Deduplication parallel durch den GC Prozess ausgeführt wird.

Mit der Option -XX:+PrintStringDeduplicationStatistics können noch entsprechende Statistiken ausgegeben werden.

[GC concurrent-string-deduplication, 499.2K->0.0B(499.2K), avg 99.6%, 0.0015977 secs]
   [Last Exec: 0.0015977 secs, Idle: 7.2240827 secs, Blocked: 0/0.0000000 secs]
      [Inspected:           12803]
         [Skipped:              0(  0.0%)]
         [Hashed:           12803(100.0%)]
         [Known:                0(  0.0%)]
         [New:              12803(100.0%)    499.2K]
      [Deduplicated:        12803(100.0%)    499.2K(100.0%)]
         [Young:               10(  0.1%)    400.0B(  0.1%)]
         [Old:              12793( 99.9%)    498.8K( 99.9%)]
   [Total Exec: 16/0.1411909 secs, Idle: 16/468.3912653 secs, Blocked: 6/0.0003615 secs]
      [Inspected:          776848]
         [Skipped:              0(  0.0%)]
         [Hashed:          771088( 99.3%)]
         [Known:             4400(  0.6%)]
         [New:             772448( 99.4%)     29.4M]
      [Deduplicated:       770228( 99.7%)     29.3M( 99.6%)]
         [Young:              108(  0.0%)   4320.0B(  0.0%)]
         [Old:             770120(100.0%)     29.3M(100.0%)]
   [Table]
      [Memory Usage: 186.9K]
      [Size: 4096, Min: 1024, Max: 16777216]
      [Entries: 6597, Load: 161.1%, Cached: 12, Added: 6616, Removed: 19]
      [Resize Count: 2, Shrink Threshold: 2730(66.7%), Grow Threshold: 8192(200.0%)]
      [Rehash Count: 0, Rehash Threshold: 120, Hash Seed: 0x0]
      [Age Threshold: 3]
   [Queue]
      [Dropped: 0]

Folgender Beispielcode bricht ohne String Deduplication bei einem 64MB großem Heap mit einem Out-Of-Memory Error ab:

private final static int NUMBER_OF_STRINGS = 1000000;

public static void main(String[] args) throws InterruptedException {

    String[] stringArray=new String[NUMBER_OF_STRINGS];

    for (int i=0; i<NUMBER_OF_STRINGS; i++) {
        stringArray[i] = new String("string " + (i % 1000));
        if(i%500 == 0) {
            Thread.sleep(300);
        }
    }
    System.out.println(stringArray[0]);
}

Mit den JVM Optionen -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+PrintStringDeduplicationStatistics kann die Schleife komplett ausgeführt werden.

Short URL for this post: http://wp.me/p4nxik-2ej
This entry was posted in Java Basics, Java Runtimes - VM, Appserver & Cloud and tagged , , , . Bookmark the permalink.

Leave a Reply