azraelxuemo's Studio.

java序列化\反序列化流程分析

2023/11/16

截屏2023-11-13 22.06.51.png
截屏2023-11-13 22.08.46.png
因为大多数场景对象是不需要序列化的,所以如果这个对象要求是可以序列化的,那么就让他继承Serializable
同时java允许继承Serializable的类的父类是非Serializable的,因为Object是非Serializable的,java如果要求子类的父类也必须Serializable,那么就相当于需要两个大的继承树,一边是Serializable的继承树,一边是非Serializable的继承树,这会造成复用性降低

序列化

https://www.cnpanda.net/sec/893.html
这篇文章不错,不过中间有一些遗漏和小错误

机制

引用机制

默认情况下unshared为False,代表允许shared,即存在共享引用机制
在序列化的过程中,已经被序列化的对象会被加入到handles里
handles obj -> wire 用来记录从对象到引用的映射
如果发现某个对象已经存在于handles里,那么会把此处记成TC_REFERENCE,即记为对同一个对象的引用
比如说,一个对象他的两个字段都是同一个字符串,那么后被序列化的字段将变成引用
截屏2023-11-12 13.51.40.png
下面这种场景也会出现引用的现象
截屏2023-11-12 13.52.54.png

处理机制

他先将所有成员变量的基本信息,如name,Type等输出到输出流,writeClassDesc
然后再统一把成员变量的实际值输出到输出流,writeSerialData

继承机制

如果某个实例他存在父类
那么writeSerialData时,会先处理父类,再处理当前类

流程

image.png

  1. 预处理:如果对象值是null,那么writeNull,如果handles里已经有当前对象,那么直接记为引用对象
  2. 信息获取:获取当前序列化对象对应类的基本信息,保存为ObjectStreamClass实例–desc
    1. 如果对应类有继承关系,那么也会把super类的基本信息获取,直到遇见非Serializable的类,然后返回null
  3. 输出:把序列化对象变成输出流
    1. 把对象的基本字段变成输出流 writeClassDesc
      1. 按照从subclass->superclass的顺序递归处理,直到遇见非Serializable的类,然后返回null
    2. 非unshared的时候把当前序列化对象加入到handles(大多数情况都是非unshared)
    3. 把对象的序列化数据变成输出流,writeSerialData
      1. 按照从最后一个继承Serializable的superclass->subclass的顺序处理,先处理父类的字段,再处理子类的字段,如果重写了WriteObject,那么就调用重写的WriteObject,不然就调用默认的defaultWriteFields

预处理

截屏2023-11-12 11.38.14.png
subs obj -> replacement 的映射,如果没有找到,就返回obj
如果实例某个成员变量为NULL,那么就会在这里writeNull

如果发现obj之前已经被添加到handles里了,那么就设置为引用
截屏2023-11-12 13.43.44.png

信息获取

查找并返回给定类的类描述符,这个desc包含了类序列化的时候需要的一些基本信息,比如说非transient的fields信息和readObject\writeObject等信息
截屏2023-11-12 11.41.30.png
下面是一个示例
截屏2023-11-12 09.35.53.png

递归

在lookup里面会创造ObjectStreamClass的实例,同时在实例的构造函数会进行判断,如果存在父类,那么会尝试lookup父类,直到遇见非Serializable的类,进而把所有可序列化父类的desc获取出来截屏2023-11-12 17.36.28.png

排序

在这里获取Fields时候会进行一个排序
截屏2023-11-12 19.42.20.png
Field的类型是ObjectStreamField,调用对应的compareTo方法
截屏2023-11-13 10.03.12.png
最后调用name的compareTo,也就是String.compareTo,按照fields.name来排序(这里的name不包含fields的type)
截屏2023-11-13 10.06.18.png

输出

writeClassDesc

Writes representation of given class descriptor to stream
把类的描述信息输出到stream里
在writeClassDesc里大多数是调用writeNonProxyDesc,因为太多数类都是non-proxy
这里因为我们序列化版本是2,所以会调用writeClassDescriptor然后调用writeNonProxy函数

writeNonProxy

在writeNonProxy里首先写上类名等信息
截屏2023-11-12 09.47.43.png
把之前lookup出的类描述符里的fields字段也write出来
截屏2023-11-12 09.49.07.png
包含TypeCode,Name和TypeString

处理继承

在writeNonProxyDesc和writeProxyDesc处理的最后都会去调用
writeClassDesc(desc.getSuperDesc(), false)来把父类的desc也输出到输出里,如果不存在父类desc,那么会writeNull,如果存在那么就重复执行上述的流程
下图是简单的内容展示
截屏2023-11-12 09.44.06.png

handles.assign

截屏2023-11-12 17.43.58.png
如果使用的模式不是unshared模式,则将当前处理的obj插入到handles对象的映射表中
继承了Serializable的对象都是在这里插入handles里的

writeSerialData

Writes instance data for each serializable class of given object, from superclass to subclass.
截屏2023-11-12 19.30.07.png
返回的slots,按照superclass->subclass的顺序依次处理
如果重写了writeObject,那么就调用对应的方法,如果没有就调用defaultWriteFields
在defaultWriteFields,首先会对基础数据类型的成员变量进行赋值,具体类型入下图所示
image.png
然后再对当前类的Fields进行处理,调用writeObject0把对应fileds的值进行序列化
截屏2023-11-12 19.44.11.png

反序列化

https://www.cnpanda.net/sec/928.html
在Java中,反序列化是将之前使用序列化机制写入到文件或者传输过程中的二进制流重新构建为Java对象的过程。这个过程是通过ObjectInputStream类实现的。
在反序列化过程中,Java不会调用对象的构造方法来构造对象。相反,它会直接使用Java虚拟机(JVM)的特殊机制(ASM)来构造对象。这个机制直接在堆中创建对象实例,而不调用任何构造方法。
这样做的原因是,序列化机制的目的在于能够完全恢复对象的状态,包括那些通过构造方法设置的私有字段。如果在反序列化时调用构造方法,那么对象可能会被初始化为不一致的状态,因为构造方法可能会执行一些不仅仅是设置字段值的逻辑。
反序列化过程如下:
如果一个类实现了Serializable接口,当它的对象被反序列化时,JVM会跳过构造方法的调用,直接在内存中创建对象。
如果对象的类有一个可访问的无参数的构造方法(包括私有的),并且该类实现了Externalizable接口,则会调用无参数的构造方法来创建对象实例,然后再从流中读取数据以恢复对象状态。
反序列化时,如果涉及到对象图中有父类没有实现Serializable接口,那么在反序列化子类时,会调用没有实现Serializable接口的最近的父类的无参数构造方法,用以正确构造对象的继承结构。
为了正确恢复对象的状态,Java的序列化机制通常要求类的所有序列化字段都要在序列化时被保存,然后在反序列化时被完全恢复。如果有特殊需求,可以通过实现readObject和writeObject方法来自定义序列化和反序列化的行为。

机制

恢复机制

反序列化恢复对象主要是通过输入流中的数据恢复的,构造当前对象也只是为了开辟内存空间,所以通过asm来做,避免操作构造函数

构造机制

父类没有继承Serializable

截屏2023-11-15 21.32.23.png
这里的var3就是我们动态生成的构造函数
截屏2023-11-13 22.12.58.png
使用了asm
截屏2023-11-13 22.13.24.png
在这里可以把动态生成的class写出来
截屏2023-11-13 22.14.23.png
查看bytecode可以看到,他只是分配了aa这个对象的内存空间,同时调用了bbb这个最近的不可序列化类的构造函数
截屏2023-11-13 22.17.37.png

父类继承了Serializable

截屏2023-11-15 21.50.23.png
截屏2023-11-15 21.53.17.png
这种情况只需要先把子类内存空间分配出来,后面再通过输入流把对应的字段填写进来
截屏2023-11-15 21.51.07.png

流程

  1. 判断当前类型,走到对应处理逻辑
  2. 把输入流变成反序列化对象
    1. 把对象的基本字段从输入流恢复 readClassDesc
      1. 按照从subclass->superclass的顺序递归处理
    2. 创造对象的实例
    3. 把对象的数据从输入流中恢复出来 readSerialData
      1. 按照从superclass->subclass的顺序递归处理,先处理父类的字段,再处理子类的字段,如果重写了ReadObject,那么就调用重写的ReadObject,不然就调用默认的defaultReadFields

判断类型

根据不同类型走不同逻辑
截屏2023-11-12 19.58.02.png

把输入流变成对象

readClassDesc

首先调用到readNonProxy去将序列化的内容恢复成desc
截屏2023-11-12 20.01.03.png
在这中间会调用resolveClass,而代码往往通过重写resolveClass来进行过滤
截屏2023-11-12 20.01.51.png
在后面可以看到他会尝试在调用一次readClassDesc,这里会按照输入流进行解析,如果输入流还能读出来内容,那就代码还有Serializable的父类的字段,那么继续解析,如果读出来的是null,那就代表结束了,那么直接会writeNull,同时会调用initNonProxy把desc的一些基本信息都填入
截屏2023-11-12 20.02.46.png

实例化对象

在把desc获取出来之后,就会实例化出对应实例,注意这里本身目的只是分配空间,为后面从输入流里面读取数据并赋值做铺垫
截屏2023-11-12 20.06.04.png

readSerialData

大概逻辑也很简单,从superclass->subclass,如果重写了readObject,那么调用readObject,如果没有那么就调用defaultReadFields
截屏2023-11-15 22.12.12.png
恢复基础类型数据
截屏2023-11-15 22.14.11.png
恢复当前类的字段,因为在序列化的时候已经对字段进行排序,后面反序列化的时候也是按照这个顺序来的
截屏2023-11-15 22.14.34.png

CATALOG
  1. 1. 序列化
    1. 1.1. 机制
      1. 1.1.1. 引用机制
      2. 1.1.2. 处理机制
      3. 1.1.3. 继承机制
    2. 1.2. 流程
      1. 1.2.1. 预处理
      2. 1.2.2. 信息获取
        1. 1.2.2.1. 递归
        2. 1.2.2.2. 排序
      3. 1.2.3. 输出
        1. 1.2.3.1. writeClassDesc
          1. 1.2.3.1.1. writeNonProxy
          2. 1.2.3.1.2. 处理继承
        2. 1.2.3.2. handles.assign
        3. 1.2.3.3. writeSerialData
  2. 2. 反序列化
    1. 2.1. 机制
      1. 2.1.1. 恢复机制
      2. 2.1.2. 构造机制
        1. 2.1.2.1. 父类没有继承Serializable
        2. 2.1.2.2. 父类继承了Serializable
    2. 2.2. 流程
      1. 2.2.1. 判断类型
      2. 2.2.2. 把输入流变成对象
        1. 2.2.2.1. readClassDesc
        2. 2.2.2.2. 实例化对象
        3. 2.2.2.3. readSerialData