博客

来自 ej-technologies 的产品新闻、更新与见解。

通过 RSS 阅读,通过 电子邮件 订阅,或在 上关注我们。

Java 与 Kotlin:谁能更快地(反)序列化?

2026-08-31
作者:Ingo Kegel

在上一篇文章中,我对 Java 序列化框架进行了性能分析,发现 Apache Fory 以巨大优势击败了 Java 原生序列化和其他竞争者。这让一些人开始关注一个问题:Kotlin 的框架相比之下表现如何。Kotlin 生态拥有自己的序列化库,最著名的是具备编译期代码生成能力的 JetBrains 官方库 kotlinx.serialization,以及采用基于 KSP 的适配器生成的 Moshi。生成代码想必会胜过 Jackson 这类重度依赖反射的 Java 方案吧?

为了弄清答案,我扩展了这个对比项目,加入了一个等价的 Kotlin 数据模型,并用 JProfiler 对序列化往返进行了性能分析。结论是:模型类使用哪种语言编写,影响并不大。不过,其中有一些非常出人意料的发现。

具体数据

负载与上次相同:一个贴近真实场景的对象图,包含共享引用、集合、映射、枚举、BigDecimal 和字节数组,在 JDK 25 上进行两百万次序列化与反序列化往返。各 Kotlin 库作用于 Kotlin 数据类,Jackson Kotlin 行额外加入了 jackson-module-kotlin,Java 行则使用等价的 Java bean 模型。首先是各 JSON 配置:

配置 语言 单次往返耗时 负载大小 说明
Jackson JSON Java 25.5 µs 9,814 字节 最快的 JSON 引擎
Jackson JSON Kotlin 29.5 µs 9,814 字节 Kotlin 构造器绑定带来的 16% 开销
Moshi JSON Kotlin 31.8 µs 9,760 字节 KSP 生成的适配器
Moshi JSON Java 37.0 µs 9,760 字节 反射式适配器
kotlinx JSON Kotlin 41.1 µs 9,769 字节 与格式无关的协议
五种 JSON 配置的单次往返耗时,从 Java 模型上 25.5 微秒的 Jackson 到 41.1 微秒的 kotlinx.serialization五种 JSON 配置的单次往返耗时,从 Java 模型上 25.5 微秒的 Jackson 到 41.1 微秒的 kotlinx.serialization

其次是二进制格式:

配置 语言 单次往返耗时 负载大小 说明
Fory Java / Kotlin 3.5 µs / 3.6 µs 7,502 / 7,620 字节 Kotlin 的测试类名稍长一些
Jackson CBOR Java 12.0 µs 7,819 字节
kotlinx Protobuf Kotlin 14.5 µs 7,155 字节 负载最小,以字段编号代替字段名
kotlinx CBOR Kotlin 15.9 µs 7,797 字节
java.io Java 46.8 µs 9,152 字节 JDK 默认的二进制格式
各二进制格式的单次往返耗时,从 3.5 微秒的 Fory 到 46.8 微秒的 java.io各二进制格式的单次往返耗时,从 3.5 微秒的 Fory 到 46.8 微秒的 java.io

Jackson 是 JSON 冠军

真正让我感到意外的是,拥有编译期代码生成能力的 kotlinx.serialization,竟然是整个对比中最慢的 JSON 后端。它输给了 Jackson 基于反射的数据绑定,甚至不及 jackson-kotlin 那一轮的运行结果。

第二个意外结果:如果你需要在 Kotlin(JVM)上进行 JSON 序列化,那么 Jackson 借助其 jackson-kotlin 模块便是首选。它比 Java 模型慢约 16%,原因在于 KotlinValueInstantiator.createFromObjectWith(7.3%)和 ValueCreator.getValueParameters(3.3%)。这就是按照 Kotlin 的方式——利用参数名、空安全和默认值——进行实例化的代价。

Jackson 为什么这么快?有几个原因叠加起来造就了这一结果。其中一点与我们在测试中使用的负载类型有关:JSON 没有二进制类型,因此测试数据中的字节数组必须进行 base64 编码。这正是各 JSON 引擎差异最大的地方:

  • Jackson 将 base64 直接编码进其 UTF-8 输出缓冲区。
  • Moshi 先创建 base64 字符串,再扫描它以处理转义,然后写入 UTF-8。
  • kotlinx.serialization 做了同样的多趟处理,而且首先将整个 JSON 文档构建为一个 String,之后再转换成字节。
kotlinx.serialization JSON 运行的火焰图,StreamingJsonEncoder.encodeString 高亮显示,在性能分析结果中占据主导地位kotlinx.serialization JSON 运行的火焰图,StreamingJsonEncoder.encodeString 高亮显示,在性能分析结果中占据主导地位

仅有代码生成还不够

kotlinx.serialization 在编译期为每个类生成序列化器,但它仍然很慢。性能分析数据清楚地揭示了原因:生成的序列化器面向的是与格式无关的 Encoder 和 Decoder 协议。每个对象都要经过带有描述符的 beginStructure,每个字段都要经过 decodeElementIndex 以及逐元素的编码和解码调用。此外,每次往返都会重新分配全新的编码器和解码器实例。

衡量这项协议开销最干净的方式是看 Protobuf 后端,因为 protobuf 的传输格式极其精简:只有字段编号和原生二进制数据。在该轮运行中,生成的反序列化器所耗费的时间约有 40% 来自解码器机制:仅 decodeElementIndex 一项就占 24.7%,beginStructure 占 15.5%。实际的数据搬运则降到了 Fory 的量级。

生成的 kotlinx.serialization 反序列化器的调用图,显示大部分时间流向 decodeElementIndex 和 beginStructure生成的 kotlinx.serialization 反序列化器的调用图,显示大部分时间流向 decodeElementIndex 和 beginStructure

所以说,kotlinx.serialization 在编译期构建代码,Fory 在运行期构建代码,但这一点对性能并无影响。关键区别在于:kotlinx.serialization 构建的是抽象序列化器,而 Fory 构建的是面向特定格式的序列化器。直接生成传输格式,才是决定性的差别。

这并不是说代码生成对性能毫不重要。如果唯一的差别就是代码生成与反射,那么它确实会有影响。

在 Kotlin 模型中,Moshi 使用 KSP 生成的适配器直接写入 JSON。在 Java 模型中,Moshi 使用反射式字段适配器,比 Kotlin 模型中的生成适配器慢 16%。

kotlinx.serialization 的优势

虽然这些数字对 kotlinx.serialization 并不有利,但它确实有不少过人之处,正因如此,我自己就是它的重度用户。

kotlinx.serialization 自带性能好得多的 CBOR 和 Protobuf 后端:Protobuf 以 14.5 µs 取得了整个对比中最小的负载(7,155 字节),CBOR 则为 15.9 µs。CBOR 解码器要为每个字段名执行一次 HashMap 查找(占该轮运行的 11%),因为 CBOR 是自描述格式,会写入字段名,而 protobuf 只使用字段编号。

首先,与格式无关的协议不是缺陷,而是特性。只要用 @Serializable 注解一个类,它就能配合 JSON、CBOR、Protobuf 以及其他第三方格式使用。其次,它可以运行在 JVM、Android、iOS、浏览器中,甚至可以在原生代码中运行。Fory 的运行期代码生成则是 JVM 专属的机制。

如果你需要 Kotlin Multiplatform,并且希望在不改动模型的前提下切换格式,kotlinx.serialization 是唯一的选择,而且其二进制后端的速度也相当可观。只是不要为了追求极致速度而选择它的 JSON 后端。

为 Kotlin 选择序列化库

  • 如果追求 JVM 上的纯粹速度,Fory 处理 Kotlin 数据类的效果与处理 Java 类完全相同,而且完全不需要任何注解。
  • 如果要在 JVM 上使用 JSON,搭配 jackson-module-kotlin 的 Jackson 是最快的选项,只需为空安全和默认值支付 16% 的 Kotlin 代价。采用 KSP 代码生成的 Moshi 紧随其后,表现同样不俗。
  • 对于 Kotlin Multiplatform,kotlinx.serialization 是默认选择。在适用的情况下,还可以选用更快的 CBOR 和 Protobuf 后端。

测量方法

测量设置与上一篇文章相同:JProfiler Gradle 插件在离线模式下为每种配置各记录一个快照,CPU 采样和内存分配记录分别在不同运行中进行。热点分析由一个编码代理驱动 JProfiler MCP 服务器 完成。完整项目见 GitHub。

如果想亲自查看这些快照,请下载 JProfiler,然后打开 build/snapshots-cpu 目录中的 .jps 文件。

归档