CodeGym /Kursy /JAVA 25 SELF /Kompresja i profilowanie serializacji

Kompresja i profilowanie serializacji

JAVA 25 SELF
Poziom 45, Lekcja 4
Dostępny

1. Wprowadzenie: po co optymalizować serializację?

W nowoczesnych aplikacjach serializacja występuje wszędzie — od protokołów sieciowych po rozproszone cache i wymianę danych między usługami.

Szybkość i rozmiar serializacji są tu kluczowe. Jeśli serializacja jest wolna, aplikacja zaczyna „spowalniać” przy zapisie lub odczycie danych, a sieć lub dysk pracują nieefektywnie. Jeśli obiekty są zbyt duże, zajmują dużo miejsca na dysku, dłużej przesyłają się przez sieć i generują dodatkowe obciążenie pamięci oraz przepustowości.

Typowe zadania obejmują zapis dużego grafu obiektów do pliku lub cache, przesyłanie obiektów przez sieć z minimalnymi opóźnieniami oraz szybkie serializowanie/deserializowanie danych w systemie wielowątkowym.

Wniosek jest prosty: optymalizacja serializacji to nie „funkcja premium”, lecz konieczna praktyka dla wydajnych i skalowalnych aplikacji.

2. Optymalizacja rozmiaru zserializowanych danych

Wykluczanie zbędnych danych: słowo kluczowe transient

Domyślnie serializowane są wszystkie pola obiektu poza oznaczonymi jako transient. Jeśli pola nie trzeba zapisywać (np. cache, dane tymczasowe, referencje do serwisów), oznacz je jako transient:

public class User implements Serializable {
    private String name;
    private transient String sessionToken; // nie będzie serializowane
}

Plusy:

  • Mniejszy rozmiar zserializowanego obiektu.
  • Brak zbędnych/ryzykownych danych w pliku lub w sieci.

Ręczna serializacja: interfejs Externalizable

Jeśli potrzebujesz pełnej kontroli nad tym, co i jak jest serializowane, zaimplementuj interfejs Externalizable i jawnie opisz serializację (metody 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);
        // nie serializujemy secret
    }

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

Plusy:

  • Serializowane są tylko potrzebne pola.
  • Można zmieniać format serializacji bez utraty kompatybilności.

Kompresja: zmniejszanie rozmiaru zserializowanych danych

Zserializowane obiekty często zajmują dużo miejsca, zwłaszcza gdy zawierają powtarzające się łańcuchy i duże kolekcje. Można zmniejszyć rozmiar za pomocą kompresji.

Przykład z GZIPOutputStream:

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

Przykład z 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();
}

Plusy:

  • Rozmiar pliku może zmniejszyć się kilkukrotnie (zwłaszcza dla dużych grafów obiektów).
  • Mniejszy ruch przy przesyle przez sieć.

Minusy:

  • Kompresja/dekompresja wymagają dodatkowego czasu (CPU).

3. Optymalizacja szybkości serializacji

Buforowanie: po co są BufferedOutputStream i BufferedInputStream

Problem:
Bez buforowania każde wywołanie write() lub read() prowadzi do systemowego dostępu do dysku lub sieci — to bardzo wolne!

Rozwiązanie:
Używaj buforowanych strumieni:

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

Plusy:

  • Znacznie przyspiesza zapis/odczyt dużych obiektów.
  • Zmniejsza liczbę operacji na dysku/sieci.

Jak to działa?
Bufor gromadzi dane w pamięci i zapisuje je porcjami, a nie bajt po bajcie.

Szybkie kopiowanie: FileChannel.transferTo

Gdy trzeba szybko skopiować duży zserializowany plik, użyj NIO i metody transferTo:

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

Plusy:

  • Kopiowanie odbywa się na poziomie systemu operacyjnego, z pominięciem zbędnego buforowania w Javie — bardzo szybkie dla dużych plików.

4. Profilowanie serializacji

Prosty pomiar czasu: System.nanoTime()

Do szybkiej oceny wydajności serializacji można użyć 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("Czas serializacji: " + (end - start) / 1_000_000 + " ms");

Plusy:

  • Prosto i szybko.
  • Można porównać różne warianty (z buforem, bez bufora, z kompresją itd.).

Minusy:

  • Wyniki mogą „skakać” z powodu pracy GC i procesów w tle.
  • Nie nadaje się do dokładnego porównania bardzo małych różnic.

Dokładne profilowanie: JMH (Java Microbenchmark Harness)

Do dokładniejszych pomiarów używaj JMH — specjalnej biblioteki do mikrobenchmarków.

Przykład prostego benchmarku:

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

Plusy:

  • Uwzględnia rozgrzewanie JVM, wpływ GC oraz „szumy” systemu operacyjnego.
  • Daje wiarygodne i powtarzalne wyniki.

Minusy:

  • Wymaga konfiguracji i zrozumienia metodologii JMH.
  • Zbyt rozbudowane do porównania „na oko”.

5. Praktyka: porównanie czasu i rozmiaru serializacji

Przeprowadźmy mini‑eksperyment: zserializujemy duży graf obiektów (np. listę z 100_000 obiektami z zagnieżdżonymi kolekcjami) różnymi sposobami i porównamy czas oraz rozmiar pliku.

Serializacja bez buforowania i kompresji

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data1.bin"))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Bez bufora: " + (end - start) / 1_000_000 + " ms, rozmiar: " +
    new File("data1.bin").length() + " bajtów");

Serializacja z buforowaniem

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("Z buforem: " + (end - start) / 1_000_000 + " ms, rozmiar: " +
    new File("data2.bin").length() + " bajtów");

Serializacja z kompresją (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("Z kompresją: " + (end - start) / 1_000_000 + " ms, rozmiar: " +
    new File("data3.gz").length() + " bajtów");

Analiza wyników

W testach serializacji wyraźnie widać, jak silnie wpływają buforowanie i kompresja. Pliki po kompresji zwykle stają się 2–10 razy mniejsze (dokładny współczynnik zależy od struktury danych). Z buforem serializacja przebiega zauważalnie szybciej, a kompresja nieco spowalnia proces, lecz oszczędność miejsca często jest tego warta.

Wniosek: przy dużych wolumenach danych koniecznie używaj buforowania, a jeśli kluczowy jest rozmiar — dołącz kompresję.

6. Typowe błędy przy optymalizacji serializacji

Błąd nr 1: Brak buforowania — serializacja dużych obiektów staje się wielokrotnie wolniejsza.

Błąd nr 2: Serializowanie zbędnych lub wrażliwych danych (np. haseł, tymczasowych tokenów) — zawsze używaj transient dla takich pól.

Błąd nr 3: Założenie, że kompresja zawsze przyspiesza serializację — w rzeczywistości kompresja zmniejsza rozmiar, ale może nieco spowolnić proces (zwłaszcza na słabszych CPU).

Błąd nr 4: Pomiar czasu bez uwzględnienia rozgrzewania JVM i wpływu GC — do dokładnych benchmarków używaj JMH.

Błąd nr 5: Porównywanie tylko czasu lub tylko rozmiaru — zawsze patrz na oba parametry, aby wybrać optymalny balans dla swojego zadania.

1
Zadanie
JAVA 25 SELF, poziom 45, lekcja 4
Niedostępne
Archiwista danych: maksymalne kompresowanie powtarzających się informacji
Archiwista danych: maksymalne kompresowanie powtarzających się informacji
1
Zadanie
JAVA 25 SELF, poziom 45, lekcja 4
Niedostępne
Twórca gry: ocena wydajności zapisu ekwipunku
Twórca gry: ocena wydajności zapisu ekwipunku
1
Ankieta/quiz
Optymalizacja binarnej serializacji, poziom 45, lekcja 4
Niedostępny
Optymalizacja binarnej serializacji
Optymalizacja binarnej serializacji
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION