CodeGym /Kurse /JAVA 25 SELF /Komprimierung und Profiling der Serialisierung

Komprimierung und Profiling der Serialisierung

JAVA 25 SELF
Level 45, Lektion 4
Verfügbar

1. Einleitung: Warum die Serialisierung optimieren?

In modernen Anwendungen begegnet uns Serialisierung überall – von Netzwerkprotokollen bis zu verteilten Caches und dem Datenaustausch zwischen Services.

Geschwindigkeit und Größe der Serialisierung sind hier kritisch. Ist die Serialisierung langsam, „stockt“ die Anwendung beim Speichern oder Laden von Daten, und Netzwerk oder Datenträger werden ineffizient genutzt. Werden Objekte zu groß, belegen sie viel Speicherplatz auf dem Datenträger, benötigen länger für die Übertragung im Netzwerk und erhöhen die Last auf Speicher und Bandbreite.

Typische Aufgaben umfassen das Speichern eines großen Objektgraphen in eine Datei oder einen Cache, die Übertragung von Objekten über das Netzwerk mit minimaler Latenz sowie eine schnelle Serialisierung/Deserialisierung von Daten in einem Multithread‑System.

Das Fazit ist einfach: Die Optimierung der Serialisierung ist kein „Premium‑Feature“, sondern eine notwendige Praxis für performante und skalierbare Anwendungen.

2. Optimierung der Größe serialisierter Daten

Überflüssige Daten ausschließen: das Schlüsselwort transient

Standardmäßig werden alle Felder eines Objekts serialisiert, außer denen, die als transient markiert sind. Wenn ein Feld nicht gespeichert werden muss (z. B. Cache, temporäre Daten, Service‑Referenzen), markieren Sie es als transient:

public class User implements Serializable {
    private String name;
    private transient String sessionToken; // wird nicht serialisiert
}

Vorteile:

  • Geringere Größe des serialisierten Objekts.
  • Keine überflüssigen/gefährlichen Daten in Datei oder Netzwerk.

Manuelle Serialisierung: das Interface Externalizable

Wenn vollständige Kontrolle darüber erforderlich ist, was und wie serialisiert wird, implementieren Sie das Interface Externalizable und definieren Sie die Serialisierung explizit (Methoden 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);
        // secret wird nicht serialisiert
    }

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

Vorteile:

  • Es werden nur die benötigten Felder serialisiert.
  • Das Serialisierungsformat lässt sich ohne Verlust der Kompatibilität ändern.

Komprimierung: serialisierte Daten verkleinern

Serialisierte Objekte benötigen oft viel Platz, insbesondere wenn sie sich wiederholende Zeichenketten und große Collections enthalten. Mit Komprimierung lässt sich die Größe verringern.

Beispiel mit GZIPOutputStream:

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

Beispiel mit 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();
}

Vorteile:

  • Die Dateigröße kann sich um ein Vielfaches verringern (insbesondere bei großen Objektgraphen).
  • Weniger Traffic bei der Übertragung über das Netzwerk.

Nachteile:

  • Komprimieren/Dekomprimieren benötigt zusätzliche Zeit (CPU).

3. Optimierung der Geschwindigkeit der Serialisierung

Pufferung: warum BufferedOutputStream und BufferedInputStream nötig sind

Problem:
Ohne Pufferung führt jeder Aufruf von write() oder read() zu einem Systemzugriff auf Datenträger oder Netzwerk – das ist sehr langsam!

Lösung:
Verwenden Sie gepufferte Streams:

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

Vorteile:

  • Beschleunigt das Schreiben/Lesen großer Objekte erheblich.
  • Reduziert die Anzahl der Zugriffe auf Datenträger/Netzwerk.

Wie funktioniert das?
Der Puffer sammelt Daten im Speicher und schreibt sie in Blöcken statt Byte für Byte.

Schnelles Kopieren: FileChannel.transferTo

Wenn eine große serialisierte Datei schnell kopiert werden soll, verwenden Sie NIO und die Methode transferTo:

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

Vorteile:

  • Das Kopieren erfolgt auf Betriebssystemebene und umgeht überflüssige Pufferung in Java – sehr schnell für große Dateien.

4. Profiling der Serialisierung

Einfache Zeitmessung: System.nanoTime()

Für eine schnelle Einschätzung der Serialisierungsleistung kann System.nanoTime() verwendet werden:

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("Serialisierungszeit: " + (end - start) / 1_000_000 + " ms");

Vorteile:

  • Einfach und schnell.
  • Vergleiche unterschiedlicher Varianten möglich (mit Puffer, ohne Puffer, mit Komprimierung etc.).

Nachteile:

  • Ergebnisse können aufgrund von GC und Hintergrundprozessen „springen“.
  • Nicht geeignet, um mikroskopisch kleine Unterschiede exakt zu vergleichen.

Präzises Profiling: JMH (Java Microbenchmark Harness)

Für genauere Messungen verwenden Sie JMH – eine spezielle Bibliothek für Microbenchmarks.

Beispiel eines einfachen Benchmarks:

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

Vorteile:

  • Berücksichtigt JVM‑Warm‑up, den Einfluss des GC und Betriebssystem‑„Rauschen“.
  • Liefert verlässliche und reproduzierbare Ergebnisse.

Nachteile:

  • Erfordert Einrichtung und Verständnis der JMH‑Methodik.
  • Überdimensioniert für einen Vergleich „nach Gefühl“.

5. Praxis: Vergleich von Zeit und Größe der Serialisierung

Lassen Sie uns ein Mini‑Experiment durchführen: Wir serialisieren einen großen Objektgraphen (z. B. eine Liste aus 100_000 Objekten mit verschachtelten Collections) auf verschiedene Arten und vergleichen Zeit und Dateigröße.

Serialisierung ohne Pufferung und Komprimierung

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data1.bin"))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Ohne Puffer: " + (end - start) / 1_000_000 + " ms, Größe: " +
    new File("data1.bin").length() + " Byte");

Serialisierung mit Pufferung

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("Mit Puffer: " + (end - start) / 1_000_000 + " ms, Größe: " +
    new File("data2.bin").length() + " Byte");

Serialisierung mit Komprimierung (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("Mit Komprimierung: " + (end - start) / 1_000_000 + " ms, Größe: " +
    new File("data3.gz").length() + " Byte");

Analyse der Ergebnisse

Bei Tests der Serialisierung zeigt sich deutlich, wie stark Pufferung und Komprimierung wirken. Komprimierte Dateien werden in der Regel um den Faktor 2–10 kleiner (der genaue Faktor hängt von der Datenstruktur ab). Mit Puffer läuft die Serialisierung merklich schneller, während die Komprimierung den Prozess etwas verlangsamt, die Platzersparnis dies jedoch oft wettmacht.

Fazit: Für große Datenmengen sollten Sie unbedingt Pufferung verwenden; ist die Größe kritisch, schalten Sie Komprimierung dazu.

6. Typische Fehler bei der Optimierung der Serialisierung

Fehler Nr. 1: Keine Pufferung verwenden – die Serialisierung großer Objekte wird um ein Vielfaches langsamer.

Fehler Nr. 2: Serialisieren unnötiger oder sensibler Daten (z. B. Passwörter, temporäre Token) – verwenden Sie für solche Felder stets transient.

Fehler Nr. 3: Die Erwartung, dass Komprimierung die Serialisierung immer beschleunigt – tatsächlich reduziert sie die Größe, kann den Prozess aber etwas verlangsamen (insbesondere auf schwachen CPUs).

Fehler Nr. 4: Zeitmessung ohne Berücksichtigung von JVM‑Warm‑up und GC‑Einfluss – für präzise Benchmarks verwenden Sie JMH.

Fehler Nr. 5: Nur die Zeit oder nur die Größe vergleichen – betrachten Sie stets beide Kennzahlen, um die optimale Balance für Ihren Anwendungsfall zu finden.

1
Aufgabe
JAVA 25 SELF, Level 45, Lektion 4
Gesperrt
Datenarchivar: maximale Komprimierung wiederkehrender Informationen
Datenarchivar: maximale Komprimierung wiederkehrender Informationen
1
Aufgabe
JAVA 25 SELF, Level 45, Lektion 4
Gesperrt
Spielentwickler: Leistungsbewertung der Inventarspeicherung
Spielentwickler: Leistungsbewertung der Inventarspeicherung
1
Umfrage/Quiz
TODO, Level 45, Lektion 4
Nicht verfügbar
TODO
TODO
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION