Stringhe compatte in Java 9

1. Panoramica

Le stringhe in Java sono rappresentate internamente da un carattere [] contenente i caratteri della stringa . Inoltre, ogni carattere è composto da 2 byte perché Java utilizza internamente UTF-16.

Ad esempio, se una stringa contiene una parola in lingua inglese, gli 8 bit iniziali saranno tutti 0 per ogni carattere , poiché un carattere ASCII può essere rappresentato utilizzando un singolo byte.

Molti caratteri richiedono 16 bit per rappresentarli, ma statisticamente la maggior parte richiede solo 8 bit - rappresentazione dei caratteri LATIN-1. Quindi, c'è un margine per migliorare il consumo di memoria e le prestazioni.

Ciò che è anche importante è che le stringhe di solito occupano gran parte dello spazio di heap JVM. E, a causa del modo in cui sono memorizzati dal JVM, nella maggior parte dei casi, una stringa istanza può richiedere fino a doppio spazio in realtà bisogno .

In questo articolo, discuteremo dell'opzione Compressed String, introdotta in JDK6 e della nuova Compact String, recentemente introdotta con JDK9. Entrambi sono stati progettati per ottimizzare il consumo di memoria delle stringhe sulla JMV.

2. Stringa compressa - Java 6

Il rilascio delle prestazioni dell'aggiornamento 21 JDK 6, ha introdotto una nuova opzione VM:

-XX:+UseCompressedStrings

Quando questa opzione è abilitata, le stringhe vengono memorizzate come byte [] , invece di char [] , risparmiando così molta memoria. Tuttavia, questa opzione è stata infine rimossa in JDK 7, principalmente perché aveva alcune conseguenze impreviste sulle prestazioni.

3. Stringa compatta - Java 9

Java 9 ha portato il concetto di stringhe compatte ba ck.

Ciò significa che ogni volta che creiamo una stringa se tutti i caratteri della stringa possono essere rappresentati utilizzando una rappresentazione byte - LATIN-1, verrà utilizzato internamente un array di byte , in modo che venga fornito un byte per un carattere.

In altri casi, se un carattere richiede più di 8 bit per rappresentarlo, tutti i caratteri vengono memorizzati utilizzando due byte per ogni rappresentazione UTF-16.

Quindi fondamentalmente, quando possibile, utilizzerà solo un singolo byte per ogni carattere.

Ora, la domanda è: come funzioneranno tutte le operazioni String ? Come distinguerà tra le rappresentazioni LATIN-1 e UTF-16?

Bene, per affrontare questo problema, viene apportata un'altra modifica all'implementazione interna di String . Abbiamo un codificatore di campo finale , che preserva queste informazioni.

3.1. Implementazione di stringhe in Java 9

Fino ad ora, la stringa era memorizzata come char [] :

private final char[] value;

D'ora in poi, sarà un byte []:

private final byte[] value;

Il codificatore di variabili :

private final byte coder;

Qualora il coder può essere:

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

La maggior parte delle operazioni String ora controlla il codificatore e invia all'implementazione specifica:

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; } 

Con tutte le informazioni necessarie a JVM pronte e disponibili, l' opzione CompactString VM è abilitata per impostazione predefinita. Per disabilitarlo possiamo usare:

+XX:-CompactStrings

3.2. Come funziona il coder

Nell'implementazione della classe Java 9 String , la lunghezza viene calcolata come:

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

Se la stringa contiene solo LATIN-1, il valore del codificatore sarà 0 quindi la lunghezza della stringa sarà la stessa della lunghezza della matrice di byte.

In altri casi, se la stringa è nella rappresentazione UTF-16, il valore di coder sarà 1 e quindi la lunghezza sarà la metà della dimensione dell'array di byte effettivo.

Si noti che tutte le modifiche apportate per Compact String sono nell'implementazione interna della classe String e sono completamente trasparenti per gli sviluppatori che utilizzano String .

4. Compatto Strings vs. compressa Strings

In caso di stringhe compresse JDK 6 , uno dei problemi principali da affrontare era che il costruttore di stringhe accettava solo char [] come argomento. Oltre a ciò, molte operazioni String dipendevano dalla rappresentazione char [] e non da un array di byte. A causa di ciò, è stato necessario disimballare molto, il che ha influito sulle prestazioni.

Mentre nel caso di Compact String, il mantenimento del "codificatore" di campo aggiuntivo può anche aumentare l'overhead. Per mitigare il costo del codificatore e lo spacchettamento dei byte in caratteri (in caso di rappresentazione UTF-16), alcuni dei metodi sono intrinseci ed è stato migliorato anche il codice ASM generato dal compilatore JIT.

Questo cambiamento ha prodotto alcuni risultati controintuitivi. LATIN-1 indexOf (String) chiama un metodo intrinseco, mentre indexOf (char) no. In caso di UTF-16, entrambi questi metodi chiamano un metodo intrinseco. Questo problema riguarda solo la stringa LATIN-1 e verrà risolto nelle versioni future.

Pertanto, le corde compatte sono migliori delle corde compresse in termini di prestazioni.

Per scoprire quanta memoria viene salvata utilizzando le stringhe compatte , sono stati analizzati vari dump dell'heap dell'applicazione Java. E, sebbene i risultati dipendessero in larga misura dalle applicazioni specifiche, i miglioramenti complessivi erano quasi sempre considerevoli.

4.1. Differenza nelle prestazioni

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.

Come sempre, l'intero codice è disponibile su Github.