Java 序列化和反序列化的底层原理

序列化和反序列化

序列化是经过某种算法将存储于内存中的对象转换成能够用于持久化存储或者通讯的形式的过程前端

反序列化是将这种被持久化存储或者通讯的数据经过对应解析算法还原成对象的过程,它是序列化的逆向操做算法

为何须要序列化

前端请求后端接口数据的时候,后端须要返回 JSON 数据,这就是后端将 Java 堆中的对象序列化为了 JSON 数据传给前端,前端能够根据自身需求直接使用或者将其反序列化为 JS 对象后端

RPC 远程调用过程当中,调用者和被调用者必须约定好序列化和反序列化算法,好比 A 应用将 User 对象序列化为了 JSON 数据传给 B 应用,User 对象数据为 {"id": 1, "name": "long"},到达 B 应用的时候须要将这些数据反序列化为对象,若是此时 B 应用的反序列化算法是 XML 的话那么确定就解析失败了,因此必须都得约定好他们都采用 JSON 序列化算法,那么基于 JSON 标准就能成功解析出 User 对象数组

Java 中的序列化

transient

若是某个字段咱们不想经过 Java 默认序列化机制输出,咱们就能够经过该字段来代表当前字段不须要被序列化性能

writeObject 和 readObject

咱们想经过自定义的方式将 address 数据序列化优化

其中的 writeObject 做用于写序列化数据的时候会反射调用该方法,readObject 会在反序列化的时候调用3d

serialVersionUID 的做用

若是咱们没有自定义 serialVersionUID 的话,会根据当前类的信息自动生成,若是当前类没有作修改那么生成的 serialVersionUID 是一致的,若是修改后 serialVersionUID 就会改变致使没法反序列化,因此在平常使用中咱们必定要填写该字段code

Java 序列化的实现原理

最开始看代码的时候,不要一下就陷入所有细节,咱们应该只看咱们目前关注的点,当认识逐渐深入以后再来看一些细节,否则的话容易看的一脸懵逼 cdn

首先调用objectOutputStream.writeObject(user); 而后调用 writeObject0(obj, false); 在这个方法里面有这样一段代码对象

这里能够看到若是咱们要序列化的是一个对象而且它没有实现 Serializable 接口的话就会直接抛出 NotSerializableException,因为咱们目前传入的是 User 对象它实现了 Serializable 因此会进入到 writeOrdinaryObject 中

首先将 TC_OBJECT 这一个对象标志位写入到流中,标识着当前开始写一个的数据是一个对象,而后调用 writeSerialData 开始写入具体数据

  1. 首先会得到 ClassDataSlot 咱们能够把它看作是提供了序列化对象的辅助手段

  2. 在此经过 ClassDataSlot 去检查序列化对象中是否实现了 writeObject 这个方法,那么这个 writeObjectMethod 是在何时初始化的呢?立刻会讲到

  3. 若是实现了 writeObject 咱们就去反射调用该方法

  4. 若是当前类没有实现 writeObject 方法就调用默认的 defaultWriteFields 去写数据

如何知道对象是否实现了 writeObject(ObjectOutputStream out) 和 readObject

在上文咱们知道是经过 writeObjectMethod 这个来判断的,那么这个字段是在哪里初始化的呢,咱们回到 ObjectOutputStream 的 writeObject0 方法,在调用后续的 writeOrderinaryObject 方法以前有这样一段代码

而后会调用到这段代码

在建立 ObjectStreamClass 对象的过程当中会经过反射去拿到当前类的方法,而后根据方法名 writeObject 和参数 ObjectOutputStream 去判断有没有这个方法,有的话就返回没有就返回为 null

而后咱们回到 defaultWriteFields(Object obj, ObjectStreamClass desc) 继续来看

  1. 拿到当前须要写入数据的具体长度
  2. 经过反射去获取当前数据的值,ObjectStreamClass desc 这个对象多是个 Object 多是基本类型等,此时拿到的是 User 对象,因此取到的值默认为空,由于这里只是写入它的具体字段的数据
  3. 经过反射拿到当前对象的全部的值
  4. 挨个调用 writeObject0 写入具体的值,首先调用的是 Integer 因为它是一个包装类,Integer 继承了 Number,Number 类实现了 Serializable 因此会和 User 对象走同样的流程到达次数(能够本身 DEBUG 一下)而后拆包取出值调用
  5. 随后就进入到了 String 的写入,再次调用 writeObject0,又到达 ObjectOutputStream 的 writeObject0,后续就有所区别了由于这里写入的具体类型是 String

由于 Integer 也实现了 Serializable 而且这里没有针对他坐特殊处理,因此它会走 writeOrdinaryObject,而 String 这里判断了,须要去调用 writeString
这个方法也比较简单首先写入 String 标志位而后写入具体的数据和长度,其它的类型也是同样会走这里不一样的分支,最终将数据写入到流中

最后来看一下序列化后占用了多少个字节

Java 反序列化的原理

反序列化其实就是序列化的逆向过程,若是你看懂了序列化的关键代码,那么看这个过程就不会很难,下面贴出关键代码作出分析

这里可以看到会根据反序列对象的具体类型分别作不一样的处理,咱们当前的对象是 User 对象因此会进入箭头指向的方法

在该方法中会去建立一个实例对象,其中 isInstantiable 这个方法是去判断构造器是否初始化了,同时这里还会将 writeObject 和 readObject 方法设置好,而后会经过 hasReadResolveMethod 方法来肯定是否实现了 readObject 方法若是实现了就反射调用 readObject 方法

在调用了 readSerialData 方法以后会调用 defaultReadFields 方法来设置字段的值,当前的 Obj 是 User 对象

  1. 获取当前传入对象数据长度因为传入的是 User 空对象,因此此时长度为空
  2. 反射获取到须要被反序列化的全部字段,而且建立对应的数组来保存对应的值,此时获取到 User 对象有 2 个字段 id 和 name
  3. 而后开始递归调用 readObject0 处理完全部须要被反序列化的字段,就一当前的 id 和 name 举例
    • 和上文序列化同样 id 是 Integer 包装类因此会被识别为 Object 当再次到达这个方法的时候,在第一步 primDataSize 数据长度为 4 由于是 int 类型 4 个字节
    • 到第二步,由于是 Integer 没有其它须要被反序列化的字段它只有自己的拆包后的值,因此会到达第四步设置当前 id 的值
  4. 设置当前基本类型字段的值

对于 String 类型来讲,在反序列化第一张中会调用读取对应的值

其它的序列化方式

一个新的技术的诞生都是有必定的缘由和背景的,好比说 Java 原生序列化后数据比较大,传输效率低,同时又又没法跨语言通讯,因此不少人选择使用 XML 的来序列化数据,XML 序列化后却是解决了跨语言通讯的问题,可是它序列化后的数据比原生数据还要大,因此就诞生了 JSON 序列化,他支持跨语言,而且序列化后的数据远远小于前 2 者,最后有人想进一步的优化大小就引入了 Protobuf 它具有 压缩的功能,被压缩的数据小于 JSON 序列化后的数据。

其它的序列化方式

  • XML
  • JSON
    • Jackson
    • FastJson
  • Hessian
  • thrift
  • protobuf
  • ...

后面会写一篇文章就会来聊聊其它的序列化方式对比下他们的性能和底层使用原理

相关文章
相关标签/搜索