Blog

Product news, updates, and insights from ej-technologies.

Read with RSS. Subscribe by email. Follow on .

Why Is Apache Fory So Fast? Profiling 5 Java Serialization Frameworks

2026-08-10
Posted by Ingo Kegel

It's no secret that the JDK architects seriously dislike Java serialization. It's a security nightmare, considerably complicates every new language feature, and on top of that it's very slow. That's why other serialization frameworks exist. There is a new kid on the block that claims impressive performance: Apache Fory. In this blog post we're going to check out if it lives up to its former name (which used to be "Fury") and compare it to the incumbents.

For the comparison, I profiled serialization and deserialization round trips of a realistic object graph with five different frameworks:

The profiling data explains why Apache Fory is in the lead and that most of what Java serialization does is not serialization.

Setup & Numbers

The test dataset consists of 200 orders with customers, addresses, order lines, products, and categories. The serialized classes contain a lot of different features that are relevant for serialization: shared references, collections, maps, enums, BigDecimal, Date, long strings and byte arrays. Each test performs two million serialize and deserialize round trips after a warmup phase, and an equals comparison verifies that every framework round-trips the data correctly. The complete project is available on GitHub.

The tests were run on JDK 25 with 2 million round trips. The JVM was profiled with the JProfiler Gradle plugin in offline mode, with CPU sampling or allocation recording, producing one snapshot per framework. For Java serialization, the java.* classes were profiled explicitly, because by default these classes are filtered out by JProfiler.

Framework Time per round trip Payload size Total allocated
Fory 1.6 3.5 µs 7,502 bytes 4.5 GB
Kryo 5.6 8.9 µs 7,257 bytes 4.2 GB
Jackson CBOR 12.0 µs 7,819 bytes 6.5 GB
Jackson JSON 28.7 µs 9,814 bytes 8.8 GB
java.io 46.8 µs 9,152 bytes 18.4 GB
Blog figureBlog figure

Java serialization is 13 times slower than Fory and allocates 4 times as much memory. This is consistent with published benchmarks. The chart also shows that the two columns are correlated: Kryo's round-trip times are nearly proportional to its allocations. Jackson JSON is slower than the allocation ratio suggests because it parses a lot of text which uses more CPU. Fory beats the correlation by a wide margin, at 7% of the time but 25% of the allocations. I will come back to that below. First, let us look at where the time actually goes.

Why Java Serialization Is Slow

Interestingly, Java serialization is very asymmetric between reading and writing: 73% of the CPU time is spent in deserialize and only 26% in serialize.

Blog figureBlog figure

The main hot spots of the run show what the time is spent on:

  • jdk.internal.misc.VM.latestUserDefinedLoader0 (18%): for every class descriptor in the stream, the JDK walks the call stack in native code to find the class loader that should resolve the class.
  • Class.forName0 (8%) and String.intern (6%): resolving those classes and interning the field names from the stream's type descriptors. Together with the class loader detection, nearly a third of the entire run is spent on answering the question "which class is this, and what fields does it have".
  • ObjectInputStream$PeekInputStream.readFully (13%): moving bytes around.
  • ObjectOutputStream$HandleTable.lookup (5%): tracking shared references.
  • BlockDataInputStream.readUTFSpan and readUTFBody (8% combined): parsing the many small UTF strings of the stream format.

The allocation data shows the same picture from the memory side. During the two million round trips, the Java serialization run allocated:

  • 8 million ObjectStreamClass instances (844 MB). Class descriptors are cached by the JDK with soft references, so the garbage collector apparently evicted a lot of entries under allocation pressure.
  • 10 million ObjectStreamField and 10.6 million FieldValues instances for metadata and field storage.
  • 26 million StringBuilder instances, mostly from descriptor parsing.
  • 21 million jdk.internal.event.DeserializationEvent instances (1.65 GB). This is JFR bookkeeping inside the JDK, allocated even though no JFR recording was active at any point.
Blog figureBlog figure

Most of this work is not related to the data being serialized. It is just the bookkeeping of the format itself: type descriptors, field metadata, and the identity tables that track object references. Creating a fresh ObjectInputStream for each round trip is the normal way to use the API, and it re-resolves class descriptors each time.

Why Fory and Kryo Are Fast

Fory and Kryo skip that bookkeeping entirely. There are no type descriptors to parse, no classes to resolve at read time, no reflection (at least in the hot path). What remains in the profiling data is mostly the work of actually building the result.

In the Fory snapshot, over a third of the time is spent directly in the serializer classes that Fory generates at runtime, visible as methods like ProductForyRefCodecCompatible4_0.readFields$. Another 14% goes to HashMap.putVal while rebuilding the attribute maps of the deserialized orders, and 6% to setting fields through VarHandles. The profiling data shows that Fory is essentially mostly moving bytes into objects.

Kryo profiling data looks different. It spends 27% in the readObject and writeObjectOrNull methods, 23% reading and writing strings, 12% in the identity maps that track references, 7% in FieldSerializer, and 5% writing class names. Kryo is fast because it does not manage descriptors like Java serialization, but it still has its own layer of per-object and per-field dispatch.

Why Fory is so Fast

What is really interesting is that Kryo allocates the least of all frameworks, yet it is 2.5 times slower than Fory. Of course, allocations just count bytes, but they do not count the instructions spent per byte.

Kryo serializes with what amounts to an interpreter loop. For every object it looks up the serializer, for every field it dispatches through FieldSerializer and for every class name it writes a string to the stream. None of these dispatch methods is a bottleneck on its own, but together they add up to over 40% of the run between the data and the bytes.

The generated codecs in Fory have no such layer. The classes that Fory compiles at runtime are just a fixed sequence of reads and writes. Buffer access goes through bulk memory operations instead of per-byte reads, and field access through VarHandles. The hot spots view shows this clearly: the top entry is HashMap.putVal from rebuilding the attribute maps, and the ranks below it are dominated by the generated ForyRefCodec classes.

Blog figureBlog figure

So the main take-away here is that it is code generation that puts Fory in a league of its own.

Different Trade-Offs for Jackson

Jackson is the most widely used serialization framework, but its selling point is not speed alone. Jackson is the default go-to solution for converting classes to JSON and has a lot of features for this purpose.

A text format like JSON is an expensive feature. Encoding the byte[] thumbnails as base64 takes 24% of the total time and decoding them another 23%, so almost half of the run is spent on the fact that JSON has no binary type. Reflective method invocation adds another 13%.

However, Jackson also has binary backends, and switching is quite simple, for example with a CBORMapper. In the example workload, CBOR is 2.4 times faster than JSON and produces payloads that are nearly as compact as Fory. The profiling data confirms that the format is no longer the problem: the CBOR parser and generator together account for about a quarter of the time, while the reflection-based data binding layer now makes up about 40%. The fact that it still has a data binding layer makes it about 3.4 times slower than Fory.

Choosing a Framework

  • For pure speed, Fory wins this comparison by a wide margin. It also has implementations for many other languages and a type-compatible mode that tolerates schema changes, which makes it a good fit for polyglot systems and for data that is stored for a long time.
  • Kryo is still a pragmatic choice for payloads like cache entries or RPC between trusted services. It is mature and has a minimal dependency footprint, but in our example workload it is 2.5 times slower than Fory.
  • For anything that humans or other tools need to read, Jackson with JSON is the pragmatic default. If you control both ends of a communication pipeline, you can switch to the CBOR backend with a one-line change.
  • Java serialization is only for compatibility with existing systems.

How This Was Profiled

Everything in this post was produced from the command line. The snapshots were recorded by the JProfiler Gradle plugin in offline mode, with one task per framework:

tasks.create<com.jprofiler.gradle.TestProfile>("profileFory") {
    useJUnitPlatform()
    filter { includeTestsMatching("com.example.serialization.ForySerializationTest") }
    offline = true
    callTreeMode = CallTreeMode.SAMPLING
    profile = listOf("com.", "org.", "java.", "javax.", "jdk.", "sun.")
    recording = listOf("cpu") // or listOf("allocation")
    snapshotFile = file("build/snapshots-cpu/fory.jps")
}

One important note: It is not a good idea to measure CPU and allocation data at the same time. Allocation recording samples every tenth object, so the absolute allocation values are understated by that factor, while the ratios between frameworks are unaffected. However, even with this sampling ratio, allocation recolrding is still expensive and distorts measured CPU times.

The analysis itself was performed by a coding agent that drove the JProfiler MCP server.

Try It Yourself

The full project is on GitHub. To look at the snapshots yourself, download JProfiler and open the .jps files from build/snapshots-cpu and build/snapshots-allocation.

Archive