用数据说话,序列化框架性能哪家强?

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

序列化是我们在日常开发中经常会使用到的技术,比如需要将内存对象持久化存储、需要将对象通过网络传输到远端。目前市面上序列化框架非常多,开发团队在进行技术选型时通常难以抉择,甚至会踩坑。

今天选择几款市面上常用的序列化框架进行测试对比,帮助开发团队搞清楚不同场景该采用哪种序列化框架。

测试对比的框架有四款:

JDK原生fastjsonKryoProtobuf

接下来会从以下这四个方面给出详细的测试对比结果:

(1)是否通用:是否支持跨语言、跨平台;

(2)是否容易使用:是否编译使用和调试;

(3)性能好不好:序列化性能主要包括时间开销和空间开销,时间开销是指序列化和反序列化对象所耗费的时间,空间开销是指序列化生成数据大小;

(3)可扩展强不强:随着业务发展,传输的业务对象可能会发生变化,比如说新增字段,这个时候就要看所选用的序列化框架是否有良好的扩展性;

框架1:JDK原生

是否通用?

JDK 原生是 Java 自带的序列化框架,与 Java 语言是强绑定的,通过 JDK 将对象序列化后是无法通过其他语言进行返序列化的,所以它的通用性比较差。

是否容易使用?

一个类实现了java.io.Serializable序列化接口就代表这个类的对象可以被序列化,否则就会报错。

简单认识一下Serializable这个类,通过看源码我们知道Serializable仅仅是一个空接口,没有定义任何方法。

public interface Serializable {
}

这说明Serializable仅仅是一个标识的作用,用来告诉 JVM 这个对象可以被序列化。

想真正完成对象序列化和反序列化还得借助 IO 核心操作类:ObjectOutputStreamObjectInputStream

ObjectOutputStream类的writeObject()方法用于将对象写入 IO 流,完成对象序列化:

  /**
     * 序列化
     *
     * @param obj 待序列化对象
     * @return 二进制字节数组
     * @throws IOException
     */

    public static byte[] serialize(Object obj) throws IOException {
        // 字节输出流
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        // 将对象序列化为二进制字节流
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(obj);
        // 获取二进制字节数组
        byte[] bytes = byteArrayOutputStream.toByteArray();
        //  关闭流
        objectOutputStream.close();
        byteArrayOutputStream.close();
        return bytes;
    }

ObjectInputStream类的readObject()方法用于从 IO 流中读取对象,完成对象反序列化:

  /**
     * 反序列化
     *
     * @param bytes 待反序列化二进制字节数组
     * @param <T> 反序列对象类型
     * @return 反序列对象
     * @throws IOException
     * @throws ClassNotFoundException
     */

    public static <T> deSerialize(byte[] bytes) throws IOException, ClassNotFoundException {
        // 字节输入流
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        // 将二进制字节流反序列化为对象
        final ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        final T object = (T) objectInputStream.readObject();
        // 关闭流
        objectInputStream.close();
        byteArrayInputStream.close();
        return object;
    }

从上面的代码可以看出,JDK 原生框架使用起来还是有点麻烦的,首先要求对象必须实现java.io.Serializable接口,其次需要借助 IO 流操作来完成序列化和反序列化。与市面上其他开源框架比起来,上面的代码写起来非常生硬。

一句话总结:JDK 原生框架易用性稍差。

性能好不好?

(1)序列化体积测试

为了方便测试对比,我定义了一个普通 java 类,后面其他框架的测试基本上也是用这个类:

public class UserDTO implements Serializable {
    private String name;
    private String wechatPub;
    private String job;
   ……
}      

将 UserDTO 类进行实例化

UserDTO userDTO = new UserDTO();
userDTO.setName("雷小帅");
userDTO.setWechatPub("微信公众号:爱笑的架构师");
userDTO.setJob("优秀码农");

序列化和反序列化测试:

System.out.println("--- 1. jdk 原生测试 ---");
byte[] bytes = JDKSerializationUtil.serialize(userDTO);
System.out.println("序列化成功:" + Arrays.toString(bytes));
System.out.println("byte size=" + bytes.length);
UserDTO userDTO1 = JDKSerializationUtil.deSerialize(bytes);
System.out.println("反序列化成功:" + userDTO1);

打印出来的结果:

--- 1. jdk 原生测试 ---
序列化成功:[-84, -19, 0, 5, 115, 114, 0, 39, ……
byte size=182
反序列化成功:UserDTO[name='雷小帅', wechatPub='微信公众号:爱笑的架构师', job='优秀码农']

一个 UserDTO 序列化完之后是 182 个字节,待会对比其他框架就知道,这个水平太差了,Java 原生是自带的序列化工具,亲儿子也不给力啊。

(2)序列化速度测试

接下来我们再测试一下序列化和反序列化的速度,总共循环 100 万次:

  • JDK 序列化耗时:2314 毫秒

  • JDK 反序列化耗时:4170 毫秒

这个成绩怎么样,后面揭晓。

可扩展强不强?

JDK 原生序列化工具通过在类中定义 serialVersionUID 常量来控制版本:

private static final long serialVersionUID = 7982581299541067770L;

上面这个serialVersionUID是通过 IDEA 工具自动生成的长整形。其实你也可以不用声明这个值,JDK 会根据 hash 算法自动生成一个。

如果序列化的时候版本号是当前这个值,反序列化前你将值改变了,那么反序列化的时候就会报错,提示 ID 不一致。

假如需要在 UserDTO 这个类再加一个字段,那如何支持扩展呢?

你可以改变一下serialVersionUID值就可以了。

框架2:fastjson

是否通用?

fastjson 是阿里巴巴出品的一款序列化框架,可以将对象序列化为 JSON 字符串,类似的框架还有 jackson, gson 等。

由于 JSON 是与语言和平台无关,因此它的通用性还是很好的。

是否容易使用?

UserDTO 类不需要实现 Serializable 接口,也不需要加 serialVersionUID 版本号,使用起来非常简单。

将一个对象序列化为 json 字符串:

com.alibaba.fastjson.JSON.toJSONString(obj);

将 json 字符串反序列化为指定类型:

com.alibaba.fastjson.JSON.parseObject(jsonString, clazz);

另外 fastjson 框架还提供了很多注解,可以在 UserDTO 类进行配置,实现一些定制化的功能需求。

性能好不好?

(1)序列化体积测试

跟 JDK 原生框架一样,假设我们已经实例化好了一个UserDTO 对象,分别进行序列化和反序列化测试:

System.out.println("--- 2. fastjson 测试 ---");
String jsonString = FastjsonSerializationUtil.serialize(userDTO);
System.out.println("序列化成功: " + jsonString);
System.out.println("byte size=" + jsonString.length());
UserDTO userDTO2 = FastjsonSerializationUtil.deSerialize(jsonString, UserDTO.class);
System.out.println("反序列化成功:" + userDTO2);

上面的代码是将序列化和反序列化代码封装到了一个工具类中。运行输出结果:

--- 2. fastjson 测试 ---
序列化成功: {"job":"优秀码农","name":"雷小帅","wechatPub":"微信公众号:爱笑的架构师"}
byte size=54
反序列化成功:UserDTO[name='雷小帅', wechatPub='微信公众号:爱笑的架构师', job='优秀码农']

可以看到序列化之后有 54 个字节,而上面 JDK 原生框架是182 个字节,对比下来发现 fastjson 确实比 JDK 原生框架强了不少,亲儿子真不行。

(2)序列化速度测试

序列化体积测试完了之后,我们再测试一下序列化和反序列化速度,经过漫长的等待,循环跑了 100 万次之后实测结果如下:

  • fastjson 序列化耗时:287 毫秒
  • fastjson 反序列化耗时:365 毫秒

这个结果简直,人如其名啊,真快~ 你看看隔壁 JDK 原生框架的速度,惨不忍睹,哎……

可扩展强不强?

fastjson 没有版本控制机制,如果对类进行修改,比如新增熟悉字段,反序列时可以进行配置,忽略不认识的熟悉字段就可以正常进行反序列化。

所以说 fastjson 的扩展性还是很灵活的。

框架3:Kryo

是否通用?

Kryo 是一个快速高效的二进制序列化框架,号称是 Java 领域最快的。它的特点是序列化速度快、体积小、接口易使用。

Kryo支持自动深/浅拷贝,它是直接通过对象->对象的深度拷贝,而不是对象->字节->对象的过程。

关于 Kryo 更多的介绍可以去 Github 查看:

https://github.com/EsotericSoftware/kryo

关于通用性,Kryo 是一款针对 Java 语言开发的框架,基本很难跨语言使用,因此通用性比较差。

是否容易使用?

先引入 Kryo 依赖:

<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>5.3.0</version>
</dependency>

Kryo 提供的 API 非常简洁,Output 类封装了输出流操作,使用 writeObject 方法将对象写入 output 输出流程即可完成二进制序列化过程。

下面代码封装了一个简单的工具方法:

/**
 * 序列化
 *
 * @param obj  待序列化对象
 * @param kryo kryo 对象
 * @return 字节数组
 */

public static byte[] serialize(Object obj, Kryo kryo) {
    Output output = new Output(1024);
    kryo.writeObject(output, obj);
    output.flush();
    return output.toBytes();
}

Kryo 反序列化也非常简单,Input 封装了输入流操作,通过 readObject 方法从输入流读取二进制反序列化成对象。

/**
 * 反序列化
 *
 * @param bytes 待反序列化二进制字节数组
 * @param <T>   反序列对象类型
 * @return 反序列对象
 */

public static <T> deSerialize(byte[] bytes, Class<T> clazz, Kryo kryo) {
    Input input = new Input(bytes);
    return kryo.readObject(input, clazz);
}

另外 Kryo 提供了丰富的配置项,可以在创建 Kryo 对象时进行配置。

总体而言,Kryo 使用起来还是非常简单的,接口易用性也是非常不错的。

性能好不好?

(1)序列化体积测试

Kryo 框架与其他框架不同,在实例化的时候可以选择提前注册类,这样序列化反序列化的速度会更快,当然也可以选择不注册。

System.out.println("--- 3. kryo 测试 ---");
Kryo kryo = new Kryo();
kryo.setRegistrationRequired(false);
// kryo.register(UserDTO.class);
byte[] kryoBytes = KryoSerializationUtil.serialize(userDTO, kryo);
System.out.println("序列化成功:" + Arrays.toString(kryoBytes));
System.out.println("byte size=" + kryoBytes.length);
UserDTO userDTO3 = KryoSerializationUtil.deSerialize(kryoBytes, UserDTO.classkryo);
System.out.println("反序列化成功:" + userDTO3);

运行结果:

序列化成功:[-123, -28, -68, -104, -25, ……]
byte size=60
反序列化成功:UserDTO[name='雷小帅', wechatPub='微信公众号:爱笑的架构师', job='优秀码农']

从结果来看,序列化后总共是 60 字节。

(2)序列化速度测试

序列化体积测试完了之后,我们再测试一下序列化和反序列化速度,经过漫长的等待,循环跑了 100 万次之后实测结果如下:

  • kryo 序列化耗时:295 毫秒
  • kryo 反序列化耗时:211 毫秒

这个成绩还不错。

可扩展强不强?

Kryo默认序列化器 FiledSerializer 是不支持字段扩展的,如果想要使用扩展序列化器则需要配置其它默认序列化器。

框架4:Protobuf

是否通用?

Protobuf 是谷歌开源的一款二进制序列化框架。

Protobuf 要求先写schema描述文件,然后通过编译器编译成具体的编程语言(Java、C++、Go 等),因此它是一种语言中立、跨平台的框架,通用性非常好。

是否容易使用?

先编写 schema 文件,定义了一个 User 类,拥有三个属性字段:

syntax = "proto3";

option java_package = "com.example.demo2.serialization.protobuf";

message User
{
string name = 1;
string wechatPub = 2;
string job = 3;
}

接着在电脑上安装好 Protobuf 编译工具,执行编译命令:

protoc --java_out=./  user-message.proto

编译成功后会生成一个 UserMessage 类。

UserMessage 类包含了很多内容:

首先有一个 Builder 内部类,可以用于实例化对象;

另外还提供了toByteArray(),可以很方便将对象序列化为二进制字节数组;提供了parseFrom()方法可以将对象反序列化为对象。

在接口使用上非常简单,开箱即用。

性能好不好?

(1)序列化体积测试

使用上面生成的UserMessage类创建一个对象,然后再进行序列化和反序列化测试:

System.out.println("--- 4. protobuf 测试 ---");
        UserMessage.User user = UserMessage.User.newBuilder()
                .setName("雷小帅")
                .setWechatPub("微信公众号:爱笑的架构师")
                .setJob("优秀码农")
                .build();

final byte[] protoBufBytes = user.toByteArray();
System.out.println("序列化成功:" + Arrays.toString(protoBufBytes));
System.out.println("byte size=" + protoBufBytes.length);
final UserMessage.User user1 = UserMessage.User.parseFrom(protoBufBytes);
System.out.println("反序列化成功:" + user1);

运行结果:

序列化成功:[-123, -28, -68, -104, -25, ……]
byte size=63
反序列化成功:UserDTO[name='雷小帅', wechatPub='微信公众号:爱笑的架构师', job='优秀码农']

序列化后是 63 字节,比 Kryo 稍微多一点点,有点吃惊。

(2)序列化速度测试

序列化体积测试完了之后,我们再测试一下序列化和反序列化速度,经过漫长的等待,循环跑了 100 万次之后实测结果如下:

  • protobuf 序列化耗时:93 毫秒
  • protobuf 反序列化耗时:341 毫秒

序列化速度很强,但是反序列化为什么慢这么多?!

可扩展强不强?

可扩展性是 Protobuf 设计目标之一,我们可以很方便进行字段增删,新旧协议都可以进行解析。

总结:

本文对常用的框架进行了测试对比,通过观察 是否通用是否容易使用性能好不好可扩展强不强 这四种维度,我们发现它们各有优劣,大家在进行技术选型时一定要慎重。

最后针对性能测试这一块,简单总结一下,给每种框架排个序。

(1)序列化体积

fastjson 54 bytes < Kryo 60 bytes < Protobuf 63 bytes < Java 原生 182 bytes

体积越小,传输效率越高,性能更优。Java 亲儿子真惨!

(2)序列化速度

protobuf 93 毫秒 < fastjson 289 毫秒 < kryo 295 毫秒 < Java 原生 2247 毫秒

Protobuf 真牛逼,王者!Java 亲儿子继续输~

(3)反序列化速度

kryo 211 毫秒 < protobuf 341 毫秒 < fastjson 396 毫秒 < Java 原生 4061 毫秒

Kryo 成绩比较稳定,序列化和反序列用时接近。Java 亲儿子输麻了!

(需要说明一下,这些测试数据是在我个人电脑上跑的,可能不够准确,仅供参考)

推荐:

主流Java进阶技术(学习资料分享)

用数据说话,序列化框架性能哪家强?
PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。“在看”支持我们吧!

原文始发于微信公众号(Java笔记虾):用数据说话,序列化框架性能哪家强?