Performance Comparison of Java Object Serialization Methods Using Chronicle Queue

This article compares Java object serialization approaches—including default Serializable, explicit SelfDescribingMarshallable, and trivially copyable techniques—by benchmarking them with JMH and Chronicle Queue, showing that explicit serialization is roughly twice as fast as default and trivially copyable is over ten times faster.

FunTester
FunTester
FunTester
Performance Comparison of Java Object Serialization Methods Using Chronicle Queue

Data Transfer Object

The article defines a FunData class as a Data Transfer Object (DTO) containing many primitive fields (long, double) to be used in the serialization experiments.

Default Serialization

Java's built‑in Serializable interface serializes objects via ObjectOutputStream and ObjectInputStream, relying on reflection to discover non‑transient fields and read/write each field, which can be performance‑heavy.

Chronicle Queue can also handle Serializable objects but offers a faster alternative through the abstract class SelfDescribingMarshallable, which still uses reflection but with lower CPU and garbage‑collection overhead.

Typical steps of default serialization are:

Identify non‑transient fields via reflection.

Read/write the identified fields via reflection.

Write the field values to the target binary format.

Example class using the default approach:

public final class DefaultFunData extends FunData {}

Explicit Serialization

Implementing private readObject and writeObject methods gives full control over the serialization process, avoiding reflection and improving speed, though any new field must be manually handled. SelfDescribingMarshallable provides a similar capability without requiring those private methods, offering two concepts: a flexible Chronicle Wire format (binary, text, YAML, JSON) and an implicit binary format for high performance.

Example of explicit serialization:

public final class ExplicitFunData extends FunData {
    @Override
    public void readMarshallable(BytesIn bytes) {
        securityId = bytes.readLong();
        time = bytes.readLong();
        bidQty0 = bytes.readDouble();
        // ... read remaining fields ...
        askPrice3 = bytes.readDouble();
    }

    @Override
    public void writeMarshallable(BytesOut bytes) {
        bytes.writeLong(securityId);
        bytes.writeLong(time);
        bytes.writeDouble(bidQty0);
        // ... write remaining fields ...
        bytes.writeDouble(askPrice3);
    }
}

Trivially Copyable

Because FunData contains only primitive fields, the JVM can lay them out contiguously in memory. By using Unsafe and memory‑copy operations, the whole object can be copied in a single step, bypassing per‑field access.

Chronicle Queue and Chronicle Bytes expose utilities to obtain the start offset and length of a trivially copyable object, enabling fast unsafe reads/writes.

Example of a trivially copyable DTO:

import static net.openhft.chronicle.bytes.BytesUtil.*;

public final class TriviallyCopyableFunData extends FunData {
    static final int START = triviallyCopyableStart(TriviallyCopyableFunData.class);
    static final int LENGTH = triviallyCopyableLength(TriviallyCopyableFunData.class);

    @Override
    public void readMarshallable(BytesIn bytes) {
        bytes.unsafeReadObject(this, START, LENGTH);
    }

    @Override
    public void writeMarshallable(BytesOut bytes) {
        bytes.unsafeWriteObject(this, START, LENGTH);
    }
}

Benchmark

The article uses JMH to benchmark serialization and deserialization of the three FunData variants on a MacBook Pro (16‑inch, 2019) with JDK 1.8.0_312 and a 2.3 GHz 8‑core Intel Core i9.

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(NANOSECONDS)
@Fork(value = 1, warmups = 1)
@Warmup(iterations = 5, time = 200, timeUnit = MILLISECONDS)
@Measurement(iterations = 5, time = 500, timeUnit = MILLISECONDS)
public class BenchmarkRunner {
    private final FunData defaultFunData = new DefaultFunData();
    private final FunData explicitFunData = new ExplicitFunData();
    private final FunData triviallyCopyableFunData = new TriviallyCopyableFunData();
    private final Bytes<Void> toBytes = Bytes.allocateElasticDirect();
    private final Bytes<Void> fromBytesDefault = Bytes.allocateElasticDirect();
    private final Bytes<Void> fromBytesExplicit = Bytes.allocateElasticDirect();
    private final Bytes<Void> fromBytesTriviallyCopyable = Bytes.allocateElasticDirect();

    public BenchmarkRunner() {
        defaultFunData.writeMarshallable(fromBytesDefault);
        explicitFunData.writeMarshallable(fromBytesExplicit);
        triviallyCopyableFunData.writeMarshallable(fromBytesTriviallyCopyable);
    }

    @Benchmark
    public void defaultWrite() {
        toBytes.writePosition(0);
        defaultFunData.writeMarshallable(toBytes);
    }

    @Benchmark
    public void defaultRead() {
        fromBytesDefault.readPosition(0);
        defaultFunData.readMarshallable(fromBytesDefault);
    }

    @Benchmark
    public void explicitWrite() {
        toBytes.writePosition(0);
        explicitFunData.writeMarshallable(toBytes);
    }

    @Benchmark
    public void explicitRead() {
        fromBytesExplicit.readPosition(0);
        explicitFunData.readMarshallable(fromBytesExplicit);
    }

    @Benchmark
    public void trivialWrite() {
        toBytes.writePosition(0);
        triviallyCopyableFunData.writeMarshallable(toBytes);
    }

    @Benchmark
    public void trivialRead() {
        fromBytesTriviallyCopyable.readPosition(0);
        triviallyCopyableFunData.readMarshallable(fromBytesTriviallyCopyable);
    }
}

Results (average time, nanoseconds per operation):

BenchmarkRunner.defaultRead    avgt    5  88.772 ± 1.766 ns/op
BenchmarkRunner.defaultWrite   avgt    5  90.679 ± 2.923 ns/op
BenchmarkRunner.explicitRead   avgt    5  32.419 ± 2.673 ns/op
BenchmarkRunner.explicitWrite  avgt    5  38.048 ± 0.778 ns/op
BenchmarkRunner.trivialRead    avgt    5   7.437 ± 0.339 ns/op
BenchmarkRunner.trivialWrite   avgt    5   7.911 ± 0.431 ns/op

Thus, explicit serialization is roughly twice as fast as the default Java serialization, while the trivially copyable approach is about four times faster than explicit and more than ten times faster than the default method.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performanceserializationchronicle-queue
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.