CodeGym /Corsi /JAVA 25 SELF /Compressione e profilazione della serializzazione

Compressione e profilazione della serializzazione

JAVA 25 SELF
Livello 45, Lezione 4
Disponibile

1. Introduzione: perché ottimizzare la serializzazione?

Nelle applicazioni moderne la serializzazione è ovunque — dai protocolli di rete alle cache distribuite e allo scambio di dati tra servizi.

La velocità e la dimensione della serializzazione sono critiche. Se la serializzazione è lenta, l’applicazione inizia a «rallentare» durante il salvataggio o il caricamento dei dati, mentre rete o disco restano inutilizzati. Se gli oggetti risultano troppo grandi, occupano molto spazio su disco, impiegano più tempo a essere trasferiti in rete e creano ulteriore carico su memoria e banda.

Compiti tipici includono il salvataggio di un grande grafo di oggetti su file o cache, la trasmissione di oggetti in rete con latenza minima e la rapida serializzazione/deserializzazione dei dati in un sistema multithread.

La conclusione è semplice: l’ottimizzazione della serializzazione non è una «funzionalità premium», ma una pratica necessaria per applicazioni performanti e scalabili.

2. Ottimizzazione della dimensione dei dati serializzati

Esclusione dei dati non necessari: parola chiave transient

Per impostazione predefinita vengono serializzati tutti i campi dell’oggetto, tranne quelli contrassegnati come transient. Se un campo non deve essere salvato (ad esempio cache, dati temporanei, riferimenti a servizi), contrassegnalo come transient:

public class User implements Serializable {
    private String name;
    private transient String sessionToken; // non verrà serializzato
}

Vantaggi:

  • Dimensioni minori dell'oggetto serializzato.
  • Niente dati superflui/pericolosi nel file o in rete.

Serializzazione manuale: interfaccia Externalizable

Se serve il pieno controllo su cosa e come viene serializzato, implementa l’interfaccia Externalizable e descrivi esplicitamente la serializzazione (metodi writeExternal/readExternal):

public class Person implements Externalizable {
    private String name;
    private int age;
    private transient String secret;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
        // non serializziamo secret
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        name = in.readUTF();
        age = in.readInt();
    }
}

Vantaggi:

  • Vengono serializzati solo i campi necessari.
  • Si può cambiare il formato di serializzazione senza perdere compatibilità.

Compressione: ridurre i dati serializzati

Gli oggetti serializzati occupano spesso molto spazio, soprattutto se contengono stringhe ripetute e collezioni grandi. È possibile ridurre la dimensione con la compressione.

Esempio con GZIPOutputStream:

try (ObjectOutputStream out = new ObjectOutputStream(
         new GZIPOutputStream(new FileOutputStream("data.gz")))) {
    out.writeObject(bigObject);
}

Esempio con ZipOutputStream:

try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream("data.zip"))) {
    zip.putNextEntry(new ZipEntry("object"));
    ObjectOutputStream out = new ObjectOutputStream(zip);
    out.writeObject(bigObject);
    out.flush();
    zip.closeEntry();
}

Vantaggi:

  • La dimensione del file può ridursi di molte volte (soprattutto per grandi grafi di oggetti).
  • Meno traffico durante il trasferimento in rete.

Svantaggi:

  • Compressione/decompressione richiedono tempo aggiuntivo (CPU).

3. Ottimizzazione della velocità di serializzazione

Bufferizzazione: perché servono BufferedOutputStream e BufferedInputStream

Problema:
Senza buffer, ogni chiamata a write() o read() comporta un accesso di sistema a disco o rete — è molto lento!

Soluzione:
Utilizza flussi bufferizzati:

try (ObjectOutputStream out = new ObjectOutputStream(
         new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    out.writeObject(bigObject);
}
try (ObjectInputStream in = new ObjectInputStream(
         new BufferedInputStream(new FileInputStream("data.bin")))) {
    Object obj = in.readObject();
}

Vantaggi:

  • Accelera significativamente la scrittura/lettura di oggetti grandi.
  • Riduce il numero di accessi a disco/rete.

Come funziona?
Il buffer accumula i dati in memoria e li scrive a blocchi, non un byte alla volta.

Copia rapida: FileChannel.transferTo

Se devi copiare velocemente un grande file serializzato, usa NIO e il metodo transferTo:

try (FileChannel src = new FileInputStream("data.bin").getChannel();
     FileChannel dest = new FileOutputStream("copy.bin").getChannel()) {
    src.transferTo(0, src.size(), dest);
}

Vantaggi:

  • La copia avviene a livello di OS, bypassando la bufferizzazione superflua in Java — molto veloce per file grandi.

4. Profilazione della serializzazione

Misurazione semplice del tempo: System.nanoTime()

Per una valutazione rapida delle prestazioni della serializzazione si può usare System.nanoTime():

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
         new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    out.writeObject(bigObject);
}
long end = System.nanoTime();
System.out.println("Tempo di serializzazione: " + (end - start) / 1_000_000 + " ms");

Vantaggi:

  • Semplice e rapido.
  • Consente di confrontare varianti diverse (con buffer, senza buffer, con compressione, ecc.).

Svantaggi:

  • I risultati possono «oscillare» a causa del lavoro del GC e dei processi in background.
  • Non adatto a confronti precisi di differenze microscopiche.

Profilazione accurata: JMH (Java Microbenchmark Harness)

Per misurazioni più accurate utilizza JMH — una libreria specifica per microbenchmark.

Esempio di benchmark semplice:

@Benchmark
public void serializeWithBuffer() throws Exception {
    try (ObjectOutputStream out = new ObjectOutputStream(
             new BufferedOutputStream(new FileOutputStream("data.bin")))) {
        out.writeObject(bigObject);
    }
}

Vantaggi:

  • Tiene conto del warm-up della JVM, dell’influenza del GC e dei «rumori» dell’OS.
  • Fornisce risultati affidabili e riproducibili.

Svantaggi:

  • Richiede configurazione e comprensione della metodologia JMH.
  • Eccessivo per confronti «a occhio».

5. Pratica: confronto di tempo e dimensione della serializzazione

Facciamo un mini‑esperimento: serializziamo un grande grafo di oggetti (ad esempio, una lista di 100_000 oggetti con collezioni annidate) in modi diversi e confrontiamo tempo e dimensione del file.

Serializzazione senza buffer e senza compressione

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data1.bin"))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Senza buffer: " + (end - start) / 1_000_000 + " ms, dimensione: " +
    new File("data1.bin").length() + " byte");

Serializzazione con buffer

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
         new BufferedOutputStream(new FileOutputStream("data2.bin")))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Con buffer: " + (end - start) / 1_000_000 + " ms, dimensione: " +
    new File("data2.bin").length() + " byte");

Serializzazione con compressione (GZIP)

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
         new GZIPOutputStream(new FileOutputStream("data3.gz")))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Con compressione: " + (end - start) / 1_000_000 + " ms, dimensione: " +
    new File("data3.gz").length() + " byte");

Analisi dei risultati

Nei test di serializzazione si nota quanto incidano bufferizzazione e compressione. I file compressi in genere diventano da 2 a 10 volte più piccoli (il fattore esatto dipende dalla struttura dei dati). Con il buffer la serializzazione è sensibilmente più veloce, mentre la compressione rallenta un po’ il processo, ma il risparmio di spazio spesso vale la pena.

Conclusione: per grandi volumi di dati usa sempre la bufferizzazione e, se la dimensione è critica, aggiungi la compressione.

6. Errori tipici nell’ottimizzazione della serializzazione

Errore n. 1: Mancato uso della bufferizzazione — la serializzazione di oggetti grandi diventa molto più lenta.

Errore n. 2: Serializzare dati non necessari o sensibili (ad esempio password, token temporanei) — usa sempre transient per tali campi.

Errore n. 3: Aspettarsi che la compressione acceleri sempre la serializzazione — in realtà la compressione riduce la dimensione, ma può rallentare un po’ il processo (soprattutto su CPU deboli).

Errore n. 4: Misurare il tempo senza considerare il warm-up della JVM e l’influenza del GC — per benchmark accurati usa JMH.

Errore n. 5: Confrontare solo il tempo o solo la dimensione — guarda sempre entrambi i parametri per scegliere il bilanciamento ottimale per il tuo caso.

1
Compito
JAVA 25 SELF, livello 45, lezione 4
Bloccato
Archivista dei dati: massima compressione delle informazioni ripetute
Archivista dei dati: massima compressione delle informazioni ripetute
1
Compito
JAVA 25 SELF, livello 45, lezione 4
Bloccato
Sviluppatore di giochi: valutazione delle prestazioni del salvataggio dell'inventario
Sviluppatore di giochi: valutazione delle prestazioni del salvataggio dell'inventario
1
Sondaggio/quiz
Ottimizzazione della serializzazione binaria, livello 45, lezione 4
Non disponibile
Ottimizzazione della serializzazione binaria
Ottimizzazione della serializzazione binaria
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION