Статьи

Оптимизация сериализации Java — Java против XML против JSON против Kryo против POF

Возможно, я наивен, но я всегда думал, что сериализация Java, безусловно, должна быть самым быстрым и эффективным способом сериализации объектов Java в двоичную форму. В конце концов, Java находится на седьмой основной версии, так что это не новая технология, и, поскольку каждый JDK кажется быстрее, чем предыдущий, я ошибочно предположил, что сериализация должна быть очень быстрой и эффективной на данный момент. Я думал, поскольку сериализация Java является двоичной и зависит от языка, она должна быть намного быстрее и эффективнее, чем XML или JSON. К сожалению, я был неправ, если вы обеспокоены производительностью, я бы рекомендовал избегать сериализации Java.

Не поймите меня неправильно, я не пытаюсь разобраться с Java. Сериализация Java имеет много требований, главное из которых — возможность сериализации чего-либо (или, по крайней мере, всего, что реализует Serializable ), в любую другую JVM (даже другую версию / реализацию JVM), даже с использованием другой версии сериализуемых классов (как пока вы устанавливаете serialVersionUID ). Главное, это просто работает, и это действительно здорово. Производительность не является основным требованием, и формат является стандартным и должен быть обратно совместимым, поэтому оптимизация очень трудна. Кроме того, для многих типов использования сериализация Java работает очень хорошо.

Я начал этот путь в недрах сериализации, работая над трехуровневым тестом параллелизма. Я заметил, что много времени процессора тратится на сериализацию Java, поэтому я решил исследовать. Я начал с сериализации простого объекта Order с несколькими полями. Я сериализовал объект и вывел байты. Хотя объект Order имел только несколько байтов данных, я не был настолько наивен, чтобы думать, что он будет сериализоваться только в несколько байтов, я знал достаточно о сериализации, что ему, по крайней мере, нужно было выписать полное имя класса, поэтому он знал что он сериализовал, чтобы он мог прочитать его обратно. Так что я ожидал, может быть, 50 байтов или около того. В результате получилось более 600 байт, и тогда я понял, что сериализация Java не так проста, как я себе представляла.

Байты сериализации Java для объекта Order

1
----sr--model.Order----h#-----J--idL--customert--Lmodel/Customer;L--descriptiont--Ljava/lang/String;L--orderLinest--Ljava/util/List;L--totalCostt--Ljava/math/BigDecimal;xp--------ppsr--java.util.ArrayListx-----a----I--sizexp----w-----sr--model.OrderLine--&-1-S----I--lineNumberL--costq-~--L--descriptionq-~--L--ordert--Lmodel/Order;xp----sr--java.math.BigDecimalT--W--(O---I--scaleL--intValt--Ljava/math/BigInteger;xr--java.lang.Number-----------xp----sr--java.math.BigInteger-----;-----I--bitCountI--bitLengthI--firstNonzeroByteNumI--lowestSetBitI--signum[--magnitudet--[Bxq-~----------------------ur--[B------T----xp----xxpq-~--xq-~--

(примечание «-» означает непечатаемый символ)

Как вы могли заметить, Java-сериализация записывает не только полное имя класса сериализуемого объекта, но также и полное определение класса сериализуемого класса, а также все ссылочные классы. Определение класса может быть довольно большим и, по-видимому, является основной проблемой производительности и эффективности, особенно при записи одного объекта. Если вы пишете большое количество объектов одного и того же класса, тогда издержки определения класса обычно не являются большой проблемой. Еще одна вещь, которую я заметил, заключается в том, что если ваш объект имеет ссылку на класс (например, объект метаданных), то в сериализации Java будет записано полное определение класса, а не только имя класса, поэтому для записи используйте сериализацию Java. наши метаданные очень дороги.

Externalizable

Можно оптимизировать сериализацию Java посредством реализации интерфейса Externalizable. Реализация этого интерфейса позволяет избежать выписывания всего определения класса, только имя класса записывается. Это требует, чтобы вы реализовали методы readExternal и writeExternal , поэтому требует некоторой работы и обслуживания с вашей стороны, но это быстрее и эффективнее, чем просто реализация Serializable.

Одно интересное замечание о результатах для Externalizable заключается в том, что он был гораздо более эффективным для небольшого числа объектов, но на самом деле выдает немного больше байтов, чем Serializable для большого количества объектов. Я предполагаю, что формат Externalizable немного менее эффективен для повторяющихся объектов.

Экстериализуемый класс

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Order implements Externalizable {
    private long id;
    private String description;
    private BigDecimal totalCost = BigDecimal.valueOf(0);
    private List orderLines = new ArrayList();
    private Customer customer;
 
    public Order() {
    }
 
    public void readExternal(ObjectInput stream) throws IOException, ClassNotFoundException {
        this.id = stream.readLong();
        this.description = (String)stream.readObject();
        this.totalCost = (BigDecimal)stream.readObject();
        this.customer = (Customer)stream.readObject();
        this.orderLines = (List)stream.readObject();
    }
 
    public void writeExternal(ObjectOutput stream) throws IOException {
        stream.writeLong(this.id);
        stream.writeObject(this.description);
        stream.writeObject(this.totalCost);
        stream.writeObject(this.customer);
        stream.writeObject(this.orderLines);
    }
}

Внешние байты сериализации для объекта Order

1
----sr--model.Order---*3--^---xpw---------psr--java.math.BigDecimalT--W--(O---I--scaleL--intValt--Ljava/math/BigInteger;xr--java.lang.Number-----------xp----sr--java.math.BigInteger-----;-----I--bitCountI--bitLengthI--firstNonzeroByteNumI--lowestSetBitI--signum[--magnitudet--[Bxq-~----------------------ur--[B------T----xp----xxpsr--java.util.ArrayListx-----a----I--sizexp----w-----sr--model.OrderLine-!!|---S---xpw-----pq-~--q-~--xxx

Другие варианты сериализации

Я начал исследовать, какие другие варианты сериализации были в Java. Я начал с EclipseLink MOXy, который поддерживает сериализацию объектов в XML или JSON через API JAXB. Я не ожидал, что сериализация XML превзойдет Java-сериализацию, поэтому был весьма удивлен, когда это произошло для определенных случаев использования. Я также нашел продукт Kryo, который является проектом с открытым исходным кодом для оптимизированной сериализации. Я также исследовал формат сериализации Oracle Coherence POF. У каждого продукта есть свои плюсы и минусы, но я сосредоточился на сравнении их производительности и эффективности.

EclipseLink MOXy — XML ​​и JSON

Основным преимуществом использования EclipseLink MOXy для сериализации в XML или JSON является то, что оба являются стандартными переносимыми форматами. Вы можете получить доступ к данным с любого клиента, используя любой язык, поэтому не ограничивайтесь Java, как при сериализации Java. Вы также можете интегрировать свои данные с веб-сервисами и сервисами REST. Оба формата также основаны на тексте, поэтому удобочитаемы. Никакого кодирования или специальных интерфейсов не требуется, только метаданные. Производительность вполне приемлема и превосходит сериализацию Java для небольших наборов данных.

Недостатком является то, что текстовые форматы менее эффективны, чем оптимизированные двоичные форматы, а JAXB требует метаданных. Поэтому вам нужно аннотировать свои классы аннотациями JAXB или предоставлять файл конфигурации XML. Кроме того, циклические ссылки не обрабатываются по умолчанию, вам нужно использовать @XmlIDREF для обработки циклов.

JAXB аннотированные классы

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@XmlRootElement
public class Order {
    @XmlID
    @XmlAttribute
    private long id;
    @XmlAttribute
    private String description;
    @XmlAttribute
    private BigDecimal totalCost = BigDecimal.valueOf(0);
    private List orderLines = new ArrayList();
    private Customer customer;
}
 
public class OrderLine {
    @XmlIDREF
    private Order order;
    @XmlAttribute
    private int lineNumber;
    @XmlAttribute
    private String description;
    @XmlAttribute
    private BigDecimal cost = BigDecimal.valueOf(0);
}

EclipseLink MOXy сериализация XML для объекта заказа

1
<order id="0" totalCost="0"><orderLines lineNumber="1" cost="0"><order>0</order></orderLines></order>

EclipseLink MOXy сериализация JSON для объекта заказа

1
{"order":{"id":0,"totalCost":0,"orderLines":[{"lineNumber":1,"cost":0,"order":0}]}}

Kryo

Kryo — это быстрая и эффективная среда сериализации для Java. Kryo — это проект с открытым исходным кодом для кода Google, который предоставляется по новой лицензии BSD. Это небольшой проект, в котором участвуют только 3 участника. Впервые он был выпущен в 2009 году, а последний был выпущен в версии 2.21 в феврале 2013 года, поэтому он все еще активно разрабатывается.

Kryo работает аналогично сериализации Java и учитывает временные поля, но не требует, чтобы класс был сериализуемым. Я обнаружил, что у Kryo есть некоторые ограничения, такие как требование к классам иметь конструктор по умолчанию, и столкнулся с некоторыми проблемами при сериализации классов java.sql.Time, java.sql.Date и java.sql.Timestamp.

Байты сериализации Kryo для объекта Order

1
------java-util-ArrayLis-----model-OrderLin----java-math-BigDecima---------model-Orde-----

Oracle Coherence POF

Продукт Oracle Coherence предоставляет собственный оптимизированный двоичный формат, называемый POF (формат переносимых объектов). Oracle Coherence — это сеточное решение для хранения данных в памяти (распределенный кеш). Coherence является коммерческим продуктом и требует лицензии. EclipseLink поддерживает интеграцию с Oracle Coherence через продукт Oracle TopLink Grid, который использует Coherence в качестве общего кэша EclipseLink.

POF обеспечивает платформу сериализации и может использоваться независимо от Coherence (если у вас уже есть лицензия Coherence). POF требует, чтобы ваш класс реализовал интерфейс PortableObject и методы чтения / записи. Вы также можете реализовать отдельный класс Serializer или использовать аннотации в последней версии Coherence. POF требует, чтобы каждому классу был заранее присвоен постоянный идентификатор, поэтому вам нужно каким-то образом определить этот идентификатор. Формат POF — это двоичный формат, очень компактный, эффективный и быстрый, но он требует определенной работы с вашей стороны.

Общее количество байтов для POF составило 32 байта для одного объекта Order / OrderLine и 1593 байта для 100 OrderLines. Я не собираюсь давать результаты, так как POF является частью коммерчески лицензированного продукта, но это было очень быстро.

POF PortableObject

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Order implements PortableObject {
    private long id;
    private String description;
    private BigDecimal totalCost = BigDecimal.valueOf(0);
    private List orderLines = new ArrayList();
    private Customer customer;
 
    public Order() {
    }
 
    public void readExternal(PofReader in) throws IOException {
        this.id = in.readLong(0);
        this.description = in.readString(1);
        this.totalCost = in.readBigDecimal(2);
        this.customer = (Customer)in.readObject(3);
        this.orderLines = (List)in.readCollection(4, new ArrayList());
    }
 
    public void writeExternal(PofWriter out) throws IOException {
        out.writeLong(0, this.id);
        out.writeString(1, this.description);
        out.writeBigDecimal(2, this.totalCost);
        out.writeObject(3, this.customer);
        out.writeCollection(4, this.orderLines);
    }
}

Байты сериализации POF для объекта Order

1
-----B--G---d-U------A--G-------

Полученные результаты

Так, как каждый выполняет? Я сделал простой тест для сравнения различных механизмов сериализации. Я сравнил сериализацию двух разных вариантов использования. Первый — это один объект Order с одним объектом OrderLine. Второй — это один объект Order с 100 объектами OrderLine. Я сравнил среднее число операций сериализации в секунду и измерил размер в байтах сериализованных данных. Разные объектные модели, варианты использования и среды дают разные результаты, но это дает вам общее представление о различиях производительности в разных сериализаторах.

Результаты показывают, что Java-сериализация медленная для небольшого числа объектов, но хороша для большого количества объектов. И наоборот, XML и JSON могут превзойти сериализацию Java для небольшого числа объектов, но сериализация Java быстрее для большого количества объектов. Kryo и другие оптимизированные двоичные сериализаторы превосходят Java-сериализацию с обоими типами данных.

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

Заказать с 1 OrderLine

Serializer Размер (в байтах) Сериализация (операций / сек) Десериализация (операций / сек) Разница в% (от сериализации Java) Разница в% (десериализация)
Java Сериализуемый 636 128634 19180 0% 0%
Java Externalizable 435 160549 26678 24% 39%
EclipseLink MOXy XML 101 348056 47334 170% 146%
Kryo 90 359368 346984 179% 1709%

Заказать через 100 строк заказа

Serializer Размер (в байтах) Сериализация (операций / сек) Десериализация (операций / сек) Разница в% (от сериализации Java) Разница в% (десериализация)
Java Сериализуемый 2715 16470 10215 0% 0%
Java Externalizable 2811 16206 11483 -1% 12%
EclipseLink MOXy XML 6628 7304 2731 -55% -73%
Kryo 1216 22862 31499 38% 208%

EclipseLink JPA

В разрабатываемых сборках EclipseLink 2.6 и до некоторой степени 2.5 мы добавили возможность выбирать ваш сериализатор везде, где EclipseLink выполняет сериализацию.

Одно из таких мест — сериализованные отображения @Lob. Теперь вы можете использовать аннотацию @Convert для указания сериализатора, такого как @Convert (XML), @Convert (JSON), @Convert (Kryo). В дополнение к оптимизации производительности это обеспечивает простой механизм записи данных XML и JSON в вашу базу данных.

Также для координации кэша EclipseLink вы можете выбрать ваш сериализатор, используя свойство eclipselink.cache.coordination.serializer.

Исходный код для тестов, используемых в этом посте, можно найти здесь или скачать здесь .