JavaRush /Курси /JAVA 25 SELF /Стиснення та профілювання серіалізації

Стиснення та профілювання серіалізації

JAVA 25 SELF
Рівень 45 , Лекція 4
Відкрита

1. Вступ: навіщо оптимізувати серіалізацію?

У сучасних застосунках серіалізація трапляється всюди — від мережевих протоколів до розподілених кешів і обміну даними між сервісами.

Швидкість і розмір серіалізації критичні. Якщо серіалізація повільна, застосунок починає «гальмувати» під час збереження або завантаження даних, а мережа чи диск простоюють даремно. Якщо об’єкти виходять надто великими, вони займають багато місця на диску, довше передаються мережею та створюють додаткове навантаження на пам’ять і пропускну здатність.

Типові завдання охоплюють збереження великого графа об’єктів у файл або кеш, передачу об’єктів мережею з мінімальною затримкою та швидку серіалізацію та десеріалізацію даних у багатопотоковій системі.

Висновок простий: оптимізація серіалізації — не «преміум‑фіча», а обов’язкова практика для продуктивних і масштабованих застосунків.

2. Оптимізація розміру серіалізованих даних

Виключення непотрібних даних: ключове слово transient

Типово серіалізуються всі поля об’єкта, окрім позначених як transient. Якщо поле не потрібно зберігати (наприклад, кеш, тимчасові дані, посилання на сервіси), позначте його як transient:

public class User implements Serializable {
    private String name;
    private transient String sessionToken; // не буде серіалізовано
}

Переваги:

  • Менший розмір серіалізованого об’єкта.
  • Немає зайвих/небезпечних даних у файлі або мережі.

Ручна серіалізація: інтерфейс Externalizable

Якщо потрібен повний контроль над тим, що і як серіалізується, реалізуйте інтерфейс Externalizable і явно опишіть процес серіалізації (методи 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 не серіалізуємо
    }

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

Переваги:

  • Серіалізуються лише потрібні поля.
  • Можна змінювати формат серіалізації без втрати сумісності.

Стиснення серіалізованих даних

Серіалізовані об’єкти часто займають багато місця, особливо якщо містять повторювані рядки та великі колекції. Можна зменшити розмір за допомогою стиснення.

Приклад із GZIPOutputStream:

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

Приклад із 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();
}

Переваги:

  • Розмір файлу може зменшитися в рази (особливо для великих графів об’єктів).
  • Менше трафіку під час передавання мережею.

Недоліки:

  • Стиснення/розпакування потребують додаткового часу (CPU).

3. Оптимізація швидкості серіалізації

Буферизація: навіщо потрібні BufferedOutputStream і BufferedInputStream

Проблема:
Без буферизації кожен виклик write() або read() призводить до системного звернення до диска або мережі — це дуже повільно!

Рішення:
Використовуйте буферизовані потоки:

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

Переваги:

  • Значно прискорює запис/читання великих об’єктів.
  • Зменшує кількість звернень до диска/мережі.

Як це працює?
Буфер накопичує дані в пам’яті та записує їх порціями, а не по одному байту.

Швидке копіювання: FileChannel.transferTo

Якщо потрібно швидко скопіювати великий серіалізований файл, використовуйте NIO і метод transferTo:

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

Переваги:

  • Копіювання відбувається на рівні ОС, минаючи зайву буферизацію в Java, — це дуже швидко для великих файлів.

4. Профілювання серіалізації

Просте вимірювання часу: System.nanoTime()

Для швидкої оцінки продуктивності серіалізації можна використовувати 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("Час серіалізації: " + (end - start) / 1_000_000 + " мс");

Переваги:

  • Просто і швидко.
  • Можна порівняти різні варіанти (з буфером, без буфера, зі стисненням тощо).

Недоліки:

  • Результати можуть «стрибати» через роботу GC і фонові процеси.
  • Не підходить для точного порівняння мікроскопічних відмінностей.

Точне профілювання: JMH (Java Microbenchmark Harness)

Для точнішого вимірювання використовуйте JMH — спеціальну бібліотеку для мікробенчмарків.

Приклад простого бенчмарка:

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

Переваги:

  • Враховує прогрів JVM, вплив GC, «шуми» ОС.
  • Дає надійні та відтворювані результати.

Недоліки:

  • Потребує налаштування та розуміння методології JMH.
  • Надмірно для порівняння «на око».

5. Практика: порівняння часу та розміру серіалізації

Проведімо міні‑експеримент: серіалізуємо великий граф об’єктів (наприклад, список із 100_000 об’єктів із вкладеними колекціями) різними способами й порівняємо час і розмір файлу.

Серіалізація без буферизації та стиснення

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data1.bin"))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Без буфера: " + (end - start) / 1_000_000 + " мс, розмір: " +
    new File("data1.bin").length() + " байт");

Серіалізація з буферизацією

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("З буфером: " + (end - start) / 1_000_000 + " мс, розмір: " +
    new File("data2.bin").length() + " байт");

Серіалізація зі стисненням (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("Зі стисненням: " + (end - start) / 1_000_000 + " мс, розмір: " +
    new File("data3.gz").length() + " байт");

Аналіз результатів

Під час тестування серіалізації добре видно, наскільки впливають буферизація та стиснення. Стиснені файли зазвичай стають у 210 разів меншими (точний коефіцієнт залежить від структури даних). З буфером серіалізація відбувається помітно швидше, а стиснення трохи уповільнює процес, але економія місця часто того варта.

Висновок: для великих обсягів даних обов’язково використовуйте буферизацію, а якщо критичним є розмір — застосовуйте стиснення.

6. Типові помилки під час оптимізації серіалізації

Помилка № 1: Невикористання буферизації — серіалізація великих об’єктів стає у рази повільнішою.

Помилка № 2: Серіалізація непотрібних або чутливих даних (наприклад, паролів, тимчасових токенів) — завжди використовуйте transient для таких полів.

Помилка № 3: Очікування, що стиснення завжди пришвидшує серіалізацію — насправді стиснення зменшує розмір, але може трохи уповільнити процес (особливо на слабких CPU).

Помилка № 4: Вимірювання часу без урахування прогріву JVM і впливу GC — для точних бенчмарків використовуйте JMH.

Помилка № 5: Порівняння лише часу або лише розміру — завжди дивіться на обидва параметри, щоб обрати оптимальний баланс для вашого завдання.

1
Опитування
Оптимізація бінарної серіалізації, рівень 45, лекція 4
Недоступний
Оптимізація бінарної серіалізації
Оптимізація бінарної серіалізації
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ