Kompakte Zeichenfolgen in Java 9

1. Übersicht

Strings in Java ist intern durch eine dargestellte char [] die Zeichen des enthaltenden String . Und jedes Zeichen besteht aus 2 Bytes, da Java intern UTF-16 verwendet.

Zum Beispiel, wenn ein String ein Wort in der englischen Sprache enthält, werden die führenden 8 Bits alle 0 sein für jedes Zeichen , wie ein ASCII - Zeichen können mit einem einzigen Byte dargestellt werden.

Viele Zeichen benötigen 16 Bit, um sie darzustellen, aber statistisch gesehen benötigen die meisten nur 8 Bit - LATIN-1-Zeichendarstellung. Es besteht also die Möglichkeit, den Speicherverbrauch und die Leistung zu verbessern.

Wichtig ist auch, dass Zeichenfolgen normalerweise einen großen Teil des JVM-Heapspeichers belegen. Und wegen der Art , wie sie von der JVM gespeichert sind, in den meisten Fällen ein String kann beispielsweise doppelt nehmen Platz es tatsächlich braucht .

In diesem Artikel werden die in JDK6 eingeführte Option für komprimierte Zeichenfolgen und die kürzlich mit JDK9 eingeführte neue kompakte Zeichenfolge erläutert. Beide wurden entwickelt, um den Speicherverbrauch von Strings auf dem JMV zu optimieren.

2. Komprimierter String - Java 6

Mit dem JDK 6 Update 21 Performance Release wurde eine neue VM-Option eingeführt:

-XX:+UseCompressedStrings

Wenn diese Option aktiviert ist, werden Strings als Byte [] anstelle von char [] gespeichert , wodurch viel Speicherplatz gespart wird. Diese Option wurde jedoch schließlich in JDK 7 entfernt, hauptsächlich weil sie einige unbeabsichtigte Auswirkungen auf die Leistung hatte.

3. Compact String - Java 9

Java 9 hat das Konzept der kompakten gebracht Strings ba ck.

Dies bedeutet, dass jedes Mal , wenn wir einen String erstellen, wenn alle Zeichen des Strings mithilfe einer Byte-LATIN-1-Darstellung dargestellt werden können, ein Byte-Array intern verwendet wird, sodass ein Byte für ein Zeichen angegeben wird.

In anderen Fällen, wenn ein Zeichen mehr als 8 Bit benötigt, um es darzustellen, werden alle Zeichen mit jeweils zwei Bytes gespeichert - UTF-16-Darstellung.

Wenn immer möglich, wird für jedes Zeichen nur ein einziges Byte verwendet.

Die Frage ist nun: Wie funktionieren alle String- Operationen? Wie wird zwischen den Darstellungen LATIN-1 und UTF-16 unterschieden?

Um dieses Problem anzugehen, wird eine weitere Änderung an der internen Implementierung des Strings vorgenommen . Wir haben einen endgültigen Feldcodierer , der diese Informationen aufbewahrt.

3.1. String- Implementierung in Java 9

Bisher wurde der String als char [] gespeichert :

private final char[] value;

Von nun an wird es ein Byte sein []:

private final byte[] value;

Der variable Codierer :

private final byte coder;

Wo der Codierer sein kann:

static final byte LATIN1 = 0; static final byte UTF16 = 1;

Die meisten String- Operationen überprüfen jetzt den Codierer und senden ihn an die spezifische Implementierung:

public int indexOf(int ch, int fromIndex) { return isLatin1() ? StringLatin1.indexOf(value, ch, fromIndex) : StringUTF16.indexOf(value, ch, fromIndex); } private boolean isLatin1() { return COMPACT_STRINGS && coder == LATIN1; } 

Wenn alle Informationen, die die JVM benötigt, bereit und verfügbar sind, ist die Option CompactString VM standardmäßig aktiviert. Um es zu deaktivieren, können wir verwenden:

+XX:-CompactStrings

3.2. Wie Codierer funktioniert

In der Implementierung der Java 9- String- Klasse wird die Länge wie folgt berechnet:

public int length() { return value.length >> coder; }

Wenn der String nur LATIN-1 enthält, ist der Wert des Codierers 0, sodass die Länge des Strings der Länge des Byte-Arrays entspricht.

In anderen Fällen ist der Wert des Codierers 1 , wenn sich der String in UTF-16-Darstellung befindet, und daher ist die Länge halb so groß wie das tatsächliche Byte-Array.

Beachten Sie, dass alle für Compact String vorgenommenen Änderungen in der internen Implementierung der String- Klasse enthalten sind und für Entwickler, die String verwenden, vollständig transparent sind .

4. Kompakte Saiten vs. komprimierte Saiten

Im Fall von JDK 6 Compressed Strings bestand ein Hauptproblem darin, dass der String- Konstruktor nur char [] als Argument akzeptierte . Darüber hinaus hingen viele String- Operationen von der Darstellung von char [] und nicht von einem Byte-Array ab. Aus diesem Grund musste viel ausgepackt werden, was sich auf die Leistung auswirkte.

Während im Fall von Compact String das Beibehalten des zusätzlichen Felds "Codierer" auch den Overhead erhöhen kann. Um die Kosten des Codierers und das Entpacken von Bytes in Zeichen (im Fall einer UTF-16-Darstellung) zu verringern , sind einige der Methoden eigen und der vom JIT-Compiler generierte ASM-Code wurde ebenfalls verbessert.

Diese Änderung führte zu einigen kontraintuitiven Ergebnissen. Der LATIN-1 indexOf (String) ruft eine intrinsische Methode auf, der indexOf (char) nicht. Im Fall von UTF-16 rufen beide Methoden eine intrinsische Methode auf. Dieses Problem betrifft nur den LATIN-1- String und wird in zukünftigen Versionen behoben.

Daher sind kompakte Saiten in Bezug auf die Leistung besser als die komprimierten Saiten .

Um herauszufinden, wie viel Speicher mithilfe der Compact Strings gespart wird , wurden verschiedene Heap-Dumps für Java-Anwendungen analysiert. Während die Ergebnisse stark von den spezifischen Anwendungen abhingen, waren die allgemeinen Verbesserungen fast immer beträchtlich.

4.1. Leistungsunterschied

Let's see a very simple example of the performance difference between enabling and disabling Compact Strings:

long startTime = System.currentTimeMillis(); List strings = IntStream.rangeClosed(1, 10_000_000) .mapToObj(Integer::toString) .collect(toList()); long totalTime = System.currentTimeMillis() - startTime; System.out.println( "Generated " + strings.size() + " strings in " + totalTime + " ms."); startTime = System.currentTimeMillis(); String appended = (String) strings.stream() .limit(100_000) .reduce("", (l, r) -> l.toString() + r.toString()); totalTime = System.currentTimeMillis() - startTime; System.out.println("Created string of length " + appended.length() + " in " + totalTime + " ms.");

Here, we are creating 10 million Strings and then appending them in a naive manner. When we run this code (Compact Strings are enabled by default), we get the output:

Generated 10000000 strings in 854 ms. Created string of length 488895 in 5130 ms.

Similarly, if we run it by disabling the Compact Strings using: -XX:-CompactStrings option, the output is:

Generated 10000000 strings in 936 ms. Created string of length 488895 in 9727 ms.

Clearly, this is a surface level test, and it can't be highly representative – it's only a snapshot of what the new option may do to improve performance in this particular scenario.

5. Conclusion

In this tutorial, we saw the attempts to optimize the performance and memory consumption on the JVM – by storing Strings in a memory efficient way.

Wie immer ist der gesamte Code auf Github verfügbar.