[Java] 序列化(Serialization)的本质是什么?在Java中怎么实现?为什么要了解序列化技术?序列化技术选型要点是什么?

文章目录

  • 前言
  • 序列化是什么?
    • 理解对象在内存中是如何存储的
    • 数据在进程内存中的分布图
    • 数据被序列化之后在内存中的分布图
  • 序列化/反序列化的本质?
  • 序列化在Java中的实现?
    • 1. JDK Serialization(不推荐使用)
    • 2. 第三方实现:Kryo
    • 3. 第三方实现:ProtoStuff 和 Protobuf
  • 为什么要了解序列化技术?
  • 序列化技术选型要点
  • 补充:byte[] vs byte stream
  • 结语

前言


序列化是相对比较重要也比较简单,但也比较容易被忽视的一类基础性知识。序列化在网络应用特别是如今的分布式系统中被大量使用,理解好序列化是理解好网络应用以及分布式系统架构的基础。笔者将通过本文去介绍序列化是什么、以及Java中的实现及选型的技术要点。

序列化是什么?


序列化,英:Serialization。序列化的定义非常简单,维基百科定义截图如下:

[Java] 序列化(Serialization)的本质是什么?在Java中怎么实现?为什么要了解序列化技术?序列化技术选型要点是什么?_第1张图片

用白话来讲就是:应用进程中的数据(数据结构或对象) 里的信息给转换成 可以进行IO的格式
进一步解释的话:

  1. 应用进程中的数据(数据结构或对象):在应用内存中数据通常是以散乱分布加指针的方式组织起来的。
  2. 可以进行IO的格式:这个一般我们认为是字节数组(byte[])。

序列化就是把在内存中散乱分布的信息(数据),把它们收集起来转换成一个byte数组来表示,并且这个过程是可逆的。那么这个数据收集和转换的过程就被称为序列化,逆向处理呢,则被称为反序列化(deserialization)

这么说可能依然难以理解,笔者会在接下来的几个章节中提供图例供大家参考。

理解对象在内存中是如何存储的

对于大部分OOP语言来说,其内部的可以说所有 实例都由两部分(对象头和对象体) 组成,它们在内存中是紧密连续排列的。对象体中原生数据类型(primitive data type)不是指针,意味着在对象的内存中能直接看到对应的原生数据类型的数据,引用类型(reference data type)则是表现为一个指针(根据jvm的具体情况可以使4字节或8字节来存储),其包含的信息是另一个对象的内存地址

也正是因为指针,让我们的数据其实在内存中是分布在多处被存储起来的,所以需要序列化技术把这些分散的数据集中起来,用 连续的字节(byte[]) 表示

当然笔者上面提到的知识点都是非常常规的理解,在Java里对象存储的时候还会有属性对齐(alignment)这种处理,本文不做详细讨论,如果有兴趣的读者可以去阅读这篇文章:《Memory Layout of Objects in Java》,这篇文章中的jol-core库在笔者验证java内置锁的几种状态时帮了大忙,十分推荐去看其Sample代码。

数据在进程内存中的分布图

在上一节我们解释了Java对象在内存中的构造,本章我们用代码和图文的去理解一下数据究竟是如何在进程内存中分布的。假设我们有这样的一个UserInfo类,包含username和uid两个属性。

[Java] 序列化(Serialization)的本质是什么?在Java中怎么实现?为什么要了解序列化技术?序列化技术选型要点是什么?_第2张图片
然后呢我们有这个类的实例如下:姓名张三,id是1。

UserInfo userInfo = new UserInfo("张三", 1);

那么这个实例在内存中则是长这样:

[Java] 序列化(Serialization)的本质是什么?在Java中怎么实现?为什么要了解序列化技术?序列化技术选型要点是什么?_第3张图片
红框里,第一个Object Header的地方代表着张三这个UserInfo实例,第二个Object Header的地方代表着"张三"这个String实例。不难看出一个对象实例若是有引用类型,其数据通常是分散存储在堆内存的各个地方。物理上就是分散的,不是连续的。

数据被序列化之后在内存中的分布图

上一节我们用图和代码的方式介绍了,对象数据在堆内存中的分布。这一节我们去看一下序列化处理后的数据在内存中长什么样。参考下图:

[Java] 序列化(Serialization)的本质是什么?在Java中怎么实现?为什么要了解序列化技术?序列化技术选型要点是什么?_第4张图片

红框里呢,就是我们序列化处理之后的结果了,简单来说对象序列化后的结果是一个byte[],也就是字节数组,Java中数组也是一个对象实例。所以你能在上图中看到Object Header,后面紧跟一串byte,代表着序列化后的字节数据。此时,因为我们对象的所有信息都被集中起来了,所以可以把这些数据通过文件IO持久化到本地文件中,也可以通过网路IO发送至远端服务器上的进程里去处理

相信到这里,基本上我们都能很清晰的认识到序列化到底是在干什么了。那么反序列化其实也同理,只不过这个过程的输出输入和序列化是反着的而已。

序列化/反序列化的本质?


上一章节我们介绍了数据的序列化反序列化的本质就是一个数据转换(Mapping)的一个过程,那么不计成本地实现起来其实就很简单了。

[Java] 序列化(Serialization)的本质是什么?在Java中怎么实现?为什么要了解序列化技术?序列化技术选型要点是什么?_第5张图片

只要你能有一套方案(算法或处理数据的方式)能够让对象实例和byte[]实现相互转换,那么这其实就是一种序列化的实现方式。只不过和JVM、哈希方法一样,可以有众多不同公司、组织的实现,不同实现的性能和特性也是有区别的,这个我们放到后面章节:《序列化技术选型要点》里讲。

就比如你甚至可以这么实现:

  1. 序列化:对象实例 ⇒ JSON对象 ⇒ JSON字符串 ⇒ byte[]
  2. 反序列化:byte[] ⇒ JSON字符串 ⇒ JSON对象 ⇒ 对象实例

虽然这种实现性能会很低,但是不可否认的是这种做法也是一种序列化和反序列化的实现。笔者仅希望大家理解序列化/反序列化的本质就是数据互相转换的一个过程。

序列化在Java中的实现?


Java中,有很多库提供了序列化的实现。本章就简单列举一下,在Java中实现序列化反序列化的一些技术。

本章并会讨论所有的序列化实现,笔者作为应用程序的开发者,很难全面了解市面上所有的序列化技术。本章节仅供参考!如果需要技术选型决策时,最好去参考相应的benchmark文章或是自家研发部根据实际需求去做调研之后再做选择!

1. JDK Serialization(不推荐使用)

比较经典的、不被看好的序列化技术,JDK自带的序列化技术。开发者可以实现 java.io.Serializable 接口。
然后用 java.io.ObjectOutputStream 类实现序列化,以及 java.io.ObjectInputStream 类实现反序列化

JDK自带的序列化技术,有很多缺点所以是 不被推荐使用的。就比如性能问题、序列化后的数据很占用空间、以及严重的反序列化安全漏洞。这个在笔者持有的《Effective Java Third Edition》书中《12. Serialization Item 85: Prefer alternatives to Java Serialization》中有详细的描述。简单来说就是当Java程序对特定的JDK序列化后的数据做反序列化时会有超过太阳生命周期的运算时间,这个漏洞可以用来攻击服务器让服务器的处理线程瘫痪以及占用大量的计算资源。有兴趣的读者可以自行去笔者提到的章节里去看。

总之,JDK序列化是非常非常不推荐使用的序列化技术。

2. 第三方实现:Kryo

Kryo是一个开源的高性能的Java序列化/反序列化的库。github传送门。Kryo库的效率很高且作为序列化结果的byte[]占用空间也较小,因此在诸多顶级项目里都有被使用。

[Java] 序列化(Serialization)的本质是什么?在Java中怎么实现?为什么要了解序列化技术?序列化技术选型要点是什么?_第6张图片

咱们国内比较热门的dubbo也悄悄咪咪列在倒数第三位。而在dubbo的文档(下图)中也是提到,如果没有跨语言序列化的需求,那么推荐使用Kryo去做Java 2 Java的序列化。

[Java] 序列化(Serialization)的本质是什么?在Java中怎么实现?为什么要了解序列化技术?序列化技术选型要点是什么?_第7张图片

3. 第三方实现:ProtoStuff 和 Protobuf

出自谷歌的Protobuf 和基于Protobuf的ProtoStuff,笔者对这两个的了解并不多,在看java序列化库benchmark时有注意到这个库的性能也是非常的高,并且是跨语言的。

[Java] 序列化(Serialization)的本质是什么?在Java中怎么实现?为什么要了解序列化技术?序列化技术选型要点是什么?_第8张图片

所谓跨语言就是你用Java编写的进程,数据序列化之后能够在别的如Python、PHP、C#等语言编写的进程内反序列化,就是所谓的跨语言。上面的Kryo就是针对Java 2 Java,不支持跨语言。而Protobuf和ProtoStuff则是支持跨语言的。我们的Protobuf支持的语言呢在上面截图里面也有提到。

Protobuf在Github上的Star数量较高,但也比较难去使用。需要自己去写.proto文件去定义消息,然后生成对应支持的语言的代码文件。相关文档链接。

[Java] 序列化(Serialization)的本质是什么?在Java中怎么实现?为什么要了解序列化技术?序列化技术选型要点是什么?_第9张图片

Protostuff则是再Protobuf的基础上,对Java语言做了特化的,用Maven导入即可,相对于Java开发者来说可能会比较友好一点,不需要自己编写.proto文件然后生成代码。也因为特化,Star数量就少了很多,但是使用起来就简单很多。

[Java] 序列化(Serialization)的本质是什么?在Java中怎么实现?为什么要了解序列化技术?序列化技术选型要点是什么?_第10张图片

为什么要了解序列化技术?


每一种技术都有其存在的意义,也就是其所对应的需求(needs)。序列化技术自然也不例外,如果你是CRUD程序员,那么基本上除了业务流程处理之外,你其实并不太可能会直接用到序列化技术。而如果你是通用组件的开发者,比如私有通信协议的开发者、rpc框架的开发者,总之一切涉及到进程间通信的开发人员,都不可避免接触到序列化技术。

亦或是你项目中用到了Redis,配置Redis也需要有序列化相关的知识。

[Java] 序列化(Serialization)的本质是什么?在Java中怎么实现?为什么要了解序列化技术?序列化技术选型要点是什么?_第11张图片

总之,在互联网领域,分布式架构之下的企业级系统呢,因为大量涉及到多个服务器上的多个进程的通信,理解序列化技术是理解分布式系统的基础

序列化技术选型要点


在选择应用哪种序列化技术时,也有一些基本的考量点。通常呢可以有这么几点:

  1. 性能:需要考虑在业务的性能需求,不用说就知道库的性能越高越好。
  2. 安全性:亦或是叫脆弱性(vulnerability),对于安全性的需求。比如前面提到的JDK 序列化有卡死线程的漏洞。
  3. 大小:序列化结果所占用的空间大小,序列化后的字节数据通常会持久化到硬盘(占存储资源)或走网络传输给其他服务器(占带宽资源),这个指标自然是越低越好。

除此之外还有如:

  1. 跨语言:企业内部系统是否有跨语言通信的需求。
  2. 可维护性:人力资源市场上,这种技术的流行程度。越流行的技术可维护性越高,维护成本越低。

补充:byte[] vs byte stream


对于序列化的结果呢,笔者本文主要使用的字节数组作为结果,那么其实并不是说结果一定是字节数组,也可以是字节流。它俩本身包含的信息是一样的(都是一串连续的字节数据),通常情况下如果数据过大,需要通过分批次处理呢,我们会选用流式的处理方式,反之如果数据不大,用byte数组也不是不可以

我们只需要知道它们所携带的信息量,本质是一样的 即可。

结语


序列化技术本身与业务代码关系并不大,反而更多与企业系统架构关系更大一点。越是趋向于分布式架构的系统,对于好的序列化技术的需求越大,而业务压力小甚至不需要做分布式架构的系统则对于序列化技术的需求没有那么大。目前来看,大部分企业都在往分布式架构系统靠的大环境下,理解序列化技术的本质也变得越发重要。

我是虎猫,希望本文对你有所帮助。(=・ω・=)

你可能感兴趣的:(#,Java,java,Serialization,序列化)