×Закрыть

Сравнение Java библиотек для сериализации

Сериализация — процесс преобразования структур данных, которые хранятся в памяти, в формат, пригодный для хранения или передачи. В ООП языках, как Java, под сериализацией объектов подразумевается преобразование состояния объекта в последовательность бит или текст. Преобразование текста или последовательности битов обратно в копию исходного объекта называется десериализацией.

Сериализация используется для передачи объектов по сети, сохранения в файлы и другие хранилища. С вопросом о сериализации часто сталкиваются при разработке распределенных приложений, в которых разные узлы кластера обмениваются данными по сети, или интеграции с внешними системами. Для обмена данные необходимо сериализировать независимо от того, как интегрируются приложения: при помощи обмена файлами, через Remote Procedure Call (RPC) или обмен сообщениями (messaging).

В данной статье приведено сравнение популярных Java библиотек для сериализции по разным критериям: производительность, размер данных сериализации, поддержка forward и backward compatibility, возможность использования в приложениях, написанных на других языках программирования.

Сегодня, когда речь заходит о сериализации, сразу вспоминаются такие модные библиотеки, как Protocol Buffers от Google, Apache Thrift, который был разработан в Facebook, и Apache Avro, разработанный в рамках Hadoop. Несмотря на то, что эти библиотеки сейчас в тренде, в сравнении они участвовать не будут по ряду причин.

Apache Thrift и Apache Avro — это фреймворки для RPC и сериализации данных. Protocol Buffers предназначен для сериализации структурированных данных и не предоставляет стандартных средств для RPC. API этих фреймворков доступны для множества языков программирования.

Так как эти фреймворки предоставляют API для разных языков программирования, сначала должна быть описана структура данных (schema), которая затем, в случае Thrift и Protocol Buffers, компилируется в классы. Avro не требует генерации кода. Структура данных в Thrift описывается в .thrift файлах, в Protocol Buffers — в .proto файлах, а в Avro — в JSON. Thrift и Protocol Buffers предоставляют компиляторы, которые компилируют .thrift и .proto файлы в классы для разных языков программирования.

Дополнительные затраты на описание структуры данных и генерацию кода не всегда оправданны. Например, когда интегрируются две подсистемы, которые написаны на Java, или приложение на Java сериализирует объект для сохранения, и нет других приложений, которые десериализируют объекты и т.д.

Также в данном сравнении не принимают участия библиотеки для сериализации в XML из-за их большого количества (которое тянет на отдельную статью). К тому же XML часто используется совместно с XSD (описанием схемы), что делает его похожим на фреймворки, которые рассматривались выше.

В данной статье будут рассмотрены библиотеки, которые позволяют сериализировать экземпляры классов, исходный код которых доступен, без необходимости описания схемы. Другими словами, аналоги Java Serialization API.

Известно, что Java Serialization является медленным (из тестов мы узнаем насколько) и сдержит уязвимости. Стандартная десериализация в Java — операция с огромной историей уязвимостей:
— Secure Coding Guidelines for Java SE;
— CWE-502: Deserialization of Untrusted Data;
— Deserialization of untrusted data.

Поэтому и возникла необходимость чем-то заменить стандартную сериализацию Java.

В тестах будут сравниваться следующие библиотеки:
— Java serialization, стандартная сериализация JDK;
— Kryo, используется в Apache Storm и Hazelcast;
— Kryo Unsafe, использует функциональность sun.misc.Unsafe;
— FST;
— FST Unsafe, использует функциональность sun.misc.Unsafe;
— Jackson JSON;
— Jackson Smile, чтение и запись данных в формате Smile («binary JSON»);
— fastjson, разработанный в Alibaba.

Сравнение форматов:

БиблиотекаФормат
БинарныйТекстовый
fastjson
FST
FST Unsafe
Jackson JSON
Jackson Smile
Java serialization
Kryo
Kryo Unsafe

Для сравнения производительности был создан проект, доступный на Gihub. Для сборки проекта используется Maven. Тесты запускаются при помощи JUnit и Maven Surefire Plugin.

Так как скорость сериализации и десериализации, а также размер результата сериализации меняется нелинейно в зависимости от структуры сериализируемого класса и количества данных в объекте, используется 3 теста, которые оперируют разными по структуре и размеру объектами. Условно выделено 3 вида объектов: маленькие, средние и большие. Объекты для тестов хранятся в трех файлах в формате JSON. В каждом файле по 100 объектов одинаковой структуры и похожего размера. Тестовые данные были созданы при помощи сервиса JSON Generator.

Сравнение размеров тестовых данных в формате JSON:

Тип объектаРазмера файла
Маленький16 KB
Средний130 KB
Большой10 MB

Тест для каждой библиотеки состоит из последовательной сериализации и десериализации 100 объектов каждого типа. Эти операции повторяются 10000 раз для минимизации погрешности. В качестве результата используется среднее арифметическое продолжительности сериализации и десериализации по всем итерациям. Для измерения времени используется высокоточный метод System.nanoTime().

Конфигурация системы, на которой запускались тесты:

ProcessorIntel® Core™ i3-4160 CPU, 3600 Mhz, 2 Cores, 4 Logical Processors
Installed Physical Memory (RAM)8 GB
Operating SystemMicrosoft Windows 7 (64-bit)
JVMJava HotSpot™ 64-Bit Server VM
JRE Version1.8.0_91
Initial Heap Size2 GB
Max Heap Size2 GB

Чтоб убедиться, что Garbage Collection (GC) не повлиял на статистику, тесты были запущены с JVM флагом “-Xloggc:gc.log”, который позволяет записать всю активность GC в файл gc.log. Из файла gc.log видно, что работа GC не могла повлиять на точность результатов тестов, всего 4 коротких запуска GC:

24.279: [GC (Metadata GC Threshold) 209944K->26531K(2010112K), 0.0612603 secs]
24.340: [Full GC (Metadata GC Threshold) 26531K->25897K(2010112K), 0.0873326 secs]
4997.614: [GC (System.gc()) 151850K->30430K(2010112K), 0.0065655 secs]
4997.620: [Full GC (System.gc()) 30430K->15343K(2010112K), 0.1173038 secs]

Далее представлены результаты тестов для трех типов объектов.

Маленькие объекты

Количество объектов — 100. Общий размер объектов в формате JSON — 16 KB.

Сравнение времени сериализации и десериализации маленьких объектов:

БиблиотекаСериализации, мсДесериализация, мсОбщее время, мсОбщее время, %
fastjson0,040,050,09100,00%
FST Unsafe0,050,060,11122,22%
FST0,040,090,13144,44%
Jackson Smile0,060,070,13144,44%
Kryo0,070,080,15166,67%
Kryo Unsafe0,070,080,15166,67%
Jackson JSON0,080,070,15166,67%
Java serialization0,210,91,111233,33%

Сравнение размера вывода сериализации маленьких объектов:

БиблиотекаРазмер вывода, KBРазмер вывода, %
Kryo6100,00%
Kryo Unsafe7116,67%
Jackson Smile10166,67%
Jackson JSON11183,33%
fastjson11183,33%
FST13216,67%
FST Unsafe21350,00%
Java serialization32533,33%

Средние объекты

Количество объектов — 100. Общий размер объектов в формате JSON — 130 KB.

Сравнение времени сериализации и десериализации средних объектов:

БиблиотекаСериализации, мсДесериализация, мсОбщее время, мсОбщее время, %
FST0,230,280,51100,00%
Kryo Unsafe0,260,270,53103,92%
Kryo0,270,280,55107,84%
FST Unsafe0,310,240,55107,84%
Jackson Smile0,270,340,61119,61%
fastjson0,390,510,9176,47%
Jackson JSON0,550,430,98192,16%
Java serialization0,912,723,63711,76%

Сравнение размера вывода сериализации средних объектов:

БиблиотекаРазмер вывода, KBРазмер вывода, %
Kryo72100,00%
Kryo Unsafe75104,17%
FST86119,44%
Jackson Smile90125,00%
Jackson JSON102141,67%
fastjson102141,67%
Java serialization145201,39%
FST Unsafe163226,39%

Большие объекты

Количество объектов — 100. Общий размер объектов в формате JSON — 10 MB.

Сравнение времени сериализации и десериализации больших объектов:

БиблиотекаСериализации, мсДесериализация, мсОбщее время, мсОбщее время, %
FST Unsafe12,17,4419,54100,00%
FST10,5310,9221,45109,77%
Jackson Smile13,1110,924,01122,88%
Kryo Unsafe28,2516,6244,87229,63%
Kryo28,6817,1145,79234,34%
Jackson JSON35,1120,9856,09287,05%
Java serialization27,0546,3273,37375,49%
fastjson52,5537,4890,03460,75%

Сравнение размера вывода сериализации больших объектов:

БиблиотекаРазмер вывода, MBРазмер вывода, %
Kryo8100,00%
Kryo Unsafe8100,00%
FST8100,00%
Jackson Smile8100,00%
Java serialization9112,50%
Jackson JSON9112,50%
fastjson9112,50%
FST Unsafe17212,50%

Вопросы совместимости

Для длительного хранения сериализованных байт может быть важно, как библиотека сериализации обрабатывает изменения в классах. Прямая совместимость (forward compatibility) — чтение сериализированных байт более новых версий классов. Обратная совместимость (backward compatibility) — чтение сериализированных байт более старых версий классов.

Java Serialization автоматически поддерживает совместимость и версионирование. Если значение статического поля serialVersionUID будет изменено, десериализация старых байт приведет к исключению:

Exception in thread "main" java.io.InvalidClassException:
serializationtest.SerializationTest$TestClass; local class incompatible: stream classdesc serialVersionUID = 42, local class serialVersionUID = 43

Список совместимых и несовместимых изменений и описание их обработки есть в официальной документации.

В Kryo по умолчанию используется FieldSerializer. FieldSerializer не поддерживает добавление, удаление или изменение типа поля без аннулирования ранее сериализованных байт.

VersionFieldSerializer позволяет добавлять аннотацию @Since(int)для указания версии, в которой было добавлено поле. Этот класс предоставляет обратную совместимость. Это означает, что новые поля могут быть добавлены, но удаление, переименование или изменение типа любого поля аннулирует ранее сериализированные байты.

TaggedFieldSerializer сериализирует только поля с аннотацией @Tag(int), предоставляя обратную совместимость, позволяя добавлять новые поля. Этот класс также предоставляет прямую совместимость, если установить setIgnoreUnknownTags(true), что позволит игнорировать любое неизвестное поле. Поля могут быть переименованы, а поля, помеченные аннотацией @Deprecated, будут проигнорированы при чтении старых байтов и не будут записаны в новые байты.

CompatibleFieldSerializer предоставляет прямую и обратную совместимость. Поля могу быть добавлены или удалены без аннулирования ранее сериализированных байт. Изменение типа поля не поддерживается. Прямая и обратная совместимость имею свою цену: первый раз, когда класс встречается в сериализованных байтах, пишется простая схема, содержащая строки с именами полей. CompatibleFieldSerializer пропускает байты, о которых он не знает.

FST поддерживает добавление полей без нарушения совместимости при помощи аннотации @Version. Для каждой версии приложения увеличивайте значение версии. Отсутствие аннотации Version означает, что версия равна 0. Каждое новое поле должно быть аннотировано. Если читается старый класс, новые поля будут инициализированы значениями по умолчанию. Удаление полей приведет к нарушению обратной совместимости, допускается только добавление полей.

JSON не требует описания схемы. Благодаря этому проще обеспечить прямую и обратную совместимость.

Чтобы обеспечить обратную совместимость в JSON всегда только добавляйте новые свойства и никогда не удаляйте и не переименовываете существующие свойства. Например, рассмотрим следующий JSON:

{
 "version": "1.0",
 "foo": true
}

Вместо переименования свойства "foo", добавьте новое свойство "bar":

{
 "version": "1.1",
 "foo": true,
 "bar": true
}

Пока вы не удаляете свойства, клиенты, основанные на более ранних версиях, продолжат работать. К недостаткам этого метода можно отнести то, что JSON может сильно раздуться со временем, а также вам необходимо поддерживать все свойства.

Jackson предоставляет прямую совместимость через установку свойства DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES объекта ObjectMapper в false. Таким образом неизвестные свойства JSON объекта будут игнорироваться.

Выводы

По результатом тестов можно сделать несколько интересных выводов. Во-первых, стандартная Java сериализация действительно худший по производительности и размеру сериализированных байт вариант. Во-вторых, скорость и размер результата сериализации зависит от размера объекта и сложности структуры класса нелинейно. Поэтому лучше заранее иметь представление о структуре данных и их размере, когда вбираете библиотеку для сериализции.

Результаты, которые показал Jackson при сериализации в JSON, меня удивили. Для небольших и средних объектов Jackson показал отличный результат с небольшим отставанием от библиотек, которые позиционируют себя как «fast» и «efficient». А учитывая то, что JSON — человекочитаемый формат, в определенных случаях Jackson может быть фаворитом при выборе библиотеки для сериализации. Несмотря на то, что на GiHub странице Fastjson от Alibaba утверждается, что эта библиотека предоставляет лучшую производительность, последний тест с большими объектами показал, что это не во всех случаях так.

LinkedIn

25 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

смешное сравнение ))))))))))) а будут ли работать эти все реализации в отличии от стандартной (Java Serialization API) правильно работать на всем том множестве систем (ОС), на которых работает стандартная JVM. Если уж сравниваете со стандартной, то прикладывайте к тестам все те системы, где может JVM работать, а то сравнили одну машину. В том, то и дело, что все забывают, что офф сборкам JVM надо поддерживать кучу систем + поддержка старых версий.

не говоря уже о том, что тут используется не jmh

прикрепили лог GC ))))))))))) ок,верим ))))))))))))))))

а будут ли работать эти все реализации в отличии от стандартной (Java Serialization API) правильно работать на всем том множестве систем (ОС), на которых работает стандартная JVM.
Будут работать с той же вероятностью, что и стандартная, может даже с большей (ибо не содержат нативного кода).
В том, то и дело, что все забывают, что офф сборкам JVM надо поддерживать кучу систем + поддержка старых версий.
Можно подробнее, о чем все забывают?

По указанной ссылке сравнение JSON сериализаторов.

спасибо за сравнение, а что насчет используемой памяти при преобразованиях? есть ли замеры? она прямо зависела от размера вывода / изменялась статистически не существенно?

Хорошее замечание. Таких замеров я не делал. Это действительно интересная информация, подумаю как добавить сбор такой информации в тесты.

Привет, Женя! Спасибо за обзор. Если есть время, скажи, пожалуйста, не сравнивал ли еще время на создание объектов? А также, не изменятся ли результаты, если использовать что-то типа LinkedHashMap кроме String, int, List..или же расширить граф объектов?

Привет, Анна. Объекты для тестов были созданы заранее 1 раз и потом сериализировались и десериализировались всеми библиотеками. Генерация объектов не учитывались в расчете времени сериализации/десериализации.
В зависимости от сложности сериализируемого объекта (object graph), скорость сериализации менялась нелинейно. Например, для простых объектов библиотка Fastjson покзала результат лучше бинарных сериализаторов, которые позиционируются, как быстрые (Kryo, FST). Но для объекта с более-менее сложной структорой Fastjson показал худший результат, заметно уступая Jackson и немного проигрывая даже Java Serialization. А вот библиотеки FST и Jackson Smile, наоборот, в этом плане приятно удивили.

У меня когда-то был достаточно подробный доклад на эту тему на JEEConf www.slideshare.net/...n-and-performance-in-java

А что насчет MessagePack? Он явно компактнее JSON’а!

Раньше не слышал о MessagePack. Спасибо, что упомянули о нем. Судя по описанию на msgpack.org — это аналог Jackson Smile.

MessagePack is an efficient binary serialization format. It lets you exchange data among multiple languages like JSON.

Действительно будет интересно сравнить производительность и размер сериализованных байтов MessagePack и Jackson Smile.

Объясните пожалуйста свою мотивацию для таких исследований.

Давно существует проект github.com/...shay/jvm-serializers/wiki в котором покрыты тестами большее кол-во протоколов.

Данный проект не давал ответы на все мои вопросы. Например, как зависит скорость сериализации/десериализации и размер сериализованных байтов от сложности структуры класса и количества данных. Кроме производительности и размера байт, в статье рассматривается еще несколько тем.

Цікаво ще б глянути на результат, якщо не використовувати бібліотек а просто вручну згенерувати той же JSON.

А також серіалізацію в джаві можна трохи оптимізувати за рахунок readExternal/writeExternal.

А також серіалізацію в джаві можна трохи оптимізувати за рахунок readExternal/writeExternal.
Это правда. Но это еще и дополнительные затраты на разработку. А это был один из аргументов, почему я не добавил в сравнение Thrift, Avro и Protocol Buffers.

Якось я також розглядав різні види серіалізації, але JSON відпав так як він займає багато місця. Мені більш підійшла серіалізація в CSV формат плюс, щоб ще стиснути Snappy алгоритм. Зара ми взагалі використовуємо ProtoBuf, але як Ви і сказали це реально трохи напряжно.

Спасибо за статью!

лол, а где gson?

Библиотек для сериализации в JSON много. Кроме GSON есть достаточно интересный Genson owlike.github.io/genson. Эти библиотеки заслуживают отдельного сравнения. Изначально я планировал в данной статье оставить только Jackson из-за его полулярности (благодаря простоте интеграции со Spring MVC), но решил добавить еще и Fastjson из-за их амбициозного заявления о лучшей производительности: «Provide best performance in server side and android client». GSON в своем GitHub подобных заявлений не делает.

The best things about Gson are:
— Provides simple toJson() and fromJson methods to convert Java objects to JSON and vice-versa
— Alow pre-existing unmodifiable objects to be converted to and from JSON
— It has extensive support of Java Generics
— Allow custom representation for objects
— Support for arbitrarily complex objects

Вообще, довольно популярная либа, FYI.

Вау, стаття про програмування. Та ще й про джаву.

Тесты запускаются при помощи JUnit и Maven Surefire Plugin.
Я правильно понял что ваши бенчмарки не используют JMH?

Подписаться на комментарии