Superare Java 8: compact strings

Superare Java 8: Compact Strings

Secondo alcuni sondaggi come quello di JetBrains, la versione 8 di Java è al momento quella più utilizzata in assoluto dagli sviluppatori di tutto il mondo, nonostante si tratti di una release del 2014.
Quello che state leggendo è il primo di una serie di articoli intitolata “Superare Java 8”, ispirata ai contenuti del mio ultimo libro “Il nuovo Java”. Questi articoli accompagneranno passo dopo passo il lettore all'esplorazione delle più importanti caratteristiche introdotte a partire dalla versione 9. L'obiettivo è quello di far acquisire la consapevolezza di quanto è importante aggiornare le proprie conoscenze relative a Java, spiegando gli enormi vantaggi che offrono le ultime versioni del linguaggio.

In questo articolo parleremo di compact strings, una meccanismo introdotto con Java 9, che rappresenta uno dei più validi motivi per abbandonare Java 8 ed aggiornarsi ad una delle versioni più recenti

Spoiler Alert

La classe String, è statisticamente la classe più utilizzata nella programmazione Java. Sembra quindi importante porsi il problema di quanto siano efficienti gli oggetti istanziati da questa classe. La buona notizia è che a partire da Java 9 tali oggetti sono nettamente più performanti rispetto alle versione precedente. Inoltre questo vantaggio si ottiene praticamente senza sforzi, ovvero basterà lanciare il nostro programma con una JVM versione 9 (o superiore), senza adottare nessun accorgimento per quanto riguarda il nostro codice. Andiamo quindi a capire cosa sono le compact strings e come utilizzarle.

Dietro le quinte


Figura 1 - File src.zip in JDK 8
Figura 1 - File src.zip in JDK 8.

Sino a Java 8, all'interno della classe String era utilizzato un array di char per memorizzare i caratteri che componeva la stringa. È possibile verificarlo leggendo il codice sorgente della classe String. Per farlo, basterà cercare il file String.java all'interno del file src.zip posizionato nella cartella di installazione del JDK versione 8 (vedi figura 1 a lato).
Tale file infatti contiene tutti i file sorgenti della libreria standard di Java.
Quindi, dopo averlo decompresso, possiamo entrare nella cartella java e poi nella cartella lang dove troveremo il sorgente della classe String.java (infatti la classe String appartiene al package java.lang).

Se apriamo tale file con un qualsiasi editor, possiamo verificare che la classe String è dichiarata come segue (abbiamo rimosso alcuni commenti ed altri elementi non utili alla nostra discussione):

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    //omesso il resto del codice

Sino a Java 8 quindi, l'esistenza dell'array di caratteri value implicava che per ogni carattere di una stringa erano allocati 16 bit (2 byte) di memoria.
In realtà, nella stragrande maggior parte delle applicazioni, vengono utilizzati caratteri che è possibile immagazzinare in solo 8 bit (1 byte). Quindi per ottenere maggiori prestazioni in termini di velocità e di utilizzo della memoria nei nostri programmi, in Java 9 l'implementazione della classe String è stata rivista per essere supportata da un array di byte invece che da un array di char. Segue la parte iniziale della dichiarazione della classe String nella versione 15 di Java, privata degli elementi non interessanti:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final byte[] value;

    /**
     * The identifier of the encoding used to encode the bytes in
     * {@code value}.
     */
    private final byte coder;
    //omesso il resto del codice

Figura 2 - File src.zip in JDK 15
Figura 2 - File src.zip in JDK 15.

Dalla versione 9 del JDK il file src.zip è stato spostato nella cartella lib (vedi Figura 2 a lato), e i package sono stati inclusi nelle cartelle che rappresentano i moduli. Quindi il sorgente String.java si trova ora sotto le cartelle java.base/java/lang. Infatti, java.base è il nome del modulo che contiene il package java.lang.
È comunque sempre possibile utilizzare caratteri meno comuni che hanno bisogno di essere immagazzinati in 16 bit (2 byte). Infatti, internamente alla classe String, è stato implementato un meccanismo basato sulla variabile coder che si occupa di allocare la giusta quantità di byte per ogni carattere.

Tale meccanismo è noto con il nome di compact strings, e dalla versione 9 di Java è il metodo utilizzato di default dalla JVM. A livello di codice non cambia nulla, utilizzeremo le stringhe come le abbiamo sempre utilizzate. Le applicazioni Java però avranno prestazioni migliori.

Davvero useremo metà della memoria per le stringhe?

Anche se abbiamo notato che oggi la classe String viene supportata da un array di byte invece che da un array di char come nella versione 8, purtroppo in Java non è possibile determinare a priori quanta memoria verrà utilizzata da un programma. Infatti, essa è gestita automaticamente dai complicati meccanismi del Garbage Collector, e ad ogni esecuzione il nostro programma potrebbe usare quantità di memoria molto differenti tra loro. Inoltre, in Java non esiste un metodo per conoscere precisamente la quantità di memoria utilizzata per un certo oggetto in un dato momento come è possibile fare con altri linguaggi. Con una strategia basata sull'interfaccia Instrumentation del package java.lang.instrument, è possibile avere un'approssimazione della dimensione di un oggetto, ma questo non vale per le stringhe che, essendo oggetti immutabili, vengono allocate in memoria in maniera differente rispetto agli altri oggetti. Quindi, anche se il meccanismo delle compact strings sembra implicare un risparmio di memoria, questo non è certo né dimostrabile. Vediamo allora quale vantaggio comporta l'utilizzo di un JDK versione 9 o superiore con un esempio di codice.

Esempio pratico

Consideriamo il seguente programma:

public class CompactStringsDemo {
    public static void main(String[] args) {
        long tempoIniziale = System.currentTimeMillis();
        long limite = 100_000;
        String s = "";
        for (int i = 0; i < limite; i++) {
            s += limite;
        }
        long tempoTotale = System.currentTimeMillis() - tempoIniziale;
        System.out.println("Create " + limite + " stringhe in " + tempoTotale
                           + "  millisecondi");
    }
}

In questa classe vengono istanziate 100.000 stringhe (che contengono proprio i primi 100.000 numeri) che vengono concatenate. Inoltre sono calcolati e stampati i millisecondi che occorrono per creare queste istanze e concatenarle.
Proviamo a lanciare l'applicazione 5 volte utilizzando il JDK versione 15.1, ed analizziamo gli output:

java CompactStringsDemo
Create 100000 stringhe in 3539 millisecondi

java CompactStringsDemo
Create 100000 stringhe in 3548 millisecondi

java CompactStringsDemo
Create 100000 stringhe in 3564 millisecondi

java CompactStringsDemo
Create 100000 stringhe in 3561 millisecondi

java CompactStringsDemo
Create 100000 stringhe in 3609 millisecondi

Possiamo osservare che per ogni lancio la velocità dell'applicazione è quasi costante, e si attesta intorno a circa 3,5 secondi.
Proviamo allora a disabilitare le compact strings utilizzando l'opzione -XX:-CompactStrings, e provare ad eseguire la stessa applicazione 5 volte per poi analizzare i risultati:

java -XX:-CompactStrings CompactStringsDemo
Create 100000 stringhe in 8731 millisecondi

java -XX:-CompactStrings CompactStringsDemo
Create 100000 stringhe in 8263 millisecondi

java -XX:-CompactStrings CompactStringsDemo
Create 100000 stringhe in 8547 millisecondi

java -XX:-CompactStrings CompactStringsDemo
Create 100000 stringhe in 8602 millisecondi

java -XX:-CompactStrings CompactStringsDemo
Create 100000 stringhe in 8353 millisecondi

Anche in questo caso, le performance in termini di velocità sono quasi costanti, ma molto peggiori rispetto a quando abbiamo usato le compact strings. Infatti, la velocità media di esecuzione di questa applicazione senza compact strings risulta essere di circa 8,5 secondi, mentre quando abbiamo utilizzato le compact strings, la media era solo di circa 3,5 secondi. Un vantaggio notevole che ci ha fatto risparmiare quasi il 60% del tempo.
Se addirittura ricompiliamo e rilanciamo il programma direttamente con l'ultima build di Java 8 (JDK 1.8.0_261), i vantaggi risultano ancora più evidenti:

"C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo
Create 100000 stringhe in 31113  millisecondi

"C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo
Create 100000 stringhe in 30376  millisecondi

"C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo
Create 100000 stringhe in 32868  millisecondi

"C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo
Create 100000 stringhe in 32508  millisecondi

"C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo
Create 100000 stringhe in 35328  millisecondi

Il peggioramento delle prestazioni questa volta è davvero notevole: con un JDK 15 e le compact strings le prestazioni dell'applicazione erano migliori di quasi 10 volte! Ovviamente, questo non significa che tutti i programmi avranno dei miglioramenti così eccezionali, perché il nostro esempio era esclusivamente basato sull'allocazione e la concatenazione di stringhe.
Per quanto riguarda il risparmio dell'utilizzo della memoria, benché probabile, come abbiamo detto non è dimostrabile visto che è il Garbage Collector svolge un lavoro complesso che si basa sulla situazione del momento.

Conclusioni

In questo articolo abbiamo visto il primo valido motivo per superare Java 8. Le compact strings introdotte a partire dalla versione 9, permettono ai nostri programmi di essere più performanti quando vengono utilizzate le stringhe. Essendo la classe String statisticamente la classe più utilizzata nei programmi Java, possiamo concludere che il solo utilizzare un JDK con versione maggiore di 8 garantirà una velocità di esecuzione maggiore alle nostre applicazioni. Abbiamo anche potuto constatare che un JDK 15 senza utilizzare le compact strings garantisce comunque prestazioni notevolmente superiori rispetto all'ultima build del JDK 8.
Aggiornare il JDK sembra proprio il primo passo da fare.

Note dell'autore

Anche ignorando la maggiore sicurezza che offrono le ultime versioni del JDK, esistono tantissimi motivi per aggiornare le proprie conoscenze di Java, o quantomeno le proprie installazioni del runtime di Java. Il mio ultimo libro “Il nuovo Java”, a cui si ispira la serie “Superare Java 8”, contiene tutte le informazioni per poter imparare Java da zero, ed utilizza un metodo didattico ben collaudato e perfezionato in oltre 20 anni di applicazione, che rende l'apprendimento semplice ed appassionante. Inoltre è strutturato per approfondire gli argomenti ed avere una conoscenza superiore che può fare la differenza per la vostra carriera lavorativa.
Per maggiori informazioni visitate https://www.nuovojava.it.

Lascia un commento

Una email sarà mandata a
claudio@claudiodesio.com


Caricamento
Il tuo messaggio è stato mandato. Grazie!