彻底理解序列化与反序列化(基于C/Java)

发布时间:2026/8/19 17:11:56
彻底理解序列化与反序列化(基于C/Java) 文章目录1. 序列化与反序列化1.1 数据的“内存格式”1.2 转换过程1.3 “可存储/可传输”的格式2. Java序列化与反序列化3. Java原生序列化与反序列化2.1 Java原生方法调用链2.2 自定义writeObject()和readObject()4. 总结5. 参考1. 序列化与反序列化我们来看一个比较常见的对序列化的定义将内存中的数据结构转换为一种可以存储或传输的格式比如字节流、JSON、XML 等即把数据从内存格式 → 转换为可传输/存储的格式。那么问题来了为什么内存中的数据不能直接传输要做这个转换的过程什么是数据的“内存格式”这个“转换”过程做了什么什么格式是“可存储/可传输”的格式弄清楚这3个概念就彻底掌握了“什么是序列化/反序列化”。1.1 数据的“内存格式”假设以下是一个进程内的地址空间地址0x0019FAC8到0x0019FACB这连续的4个字节代表什么呢答案是不确定。你可能会说这不就是是数值“1”即十六机制的10x00000001。得出这个结论你已经把这台机器的字节序默认为小端序且这是一个整数编码。关于这些硬件底层概念我们就不做过多阐释了提到这些概念是因为它们确确实实影响到了序列化这一过程。很明显直接把上面4字节数据拷贝到另一台机器上不说明它的字节序大端序/小端序、数据编码格式源码/补码/反码机器是无法正确读取并处理的。硬件底层的序列化负责字节序、浮点数标准 、内存对齐这些内容解决“这一串比特这么看”。我们来看一个C语言的结构体structperson{charname[20];// 实际是一个指针指向该数组的首地址intage;// 4字节有符号整数};那么创建一个对象struct person person1 { mingsec,18};它在内存中的结构可能是地址 0x7fff0010: 0x7fff0050 ← 这是个指针指向 name 的实际内容 地址 0x7fff0018: 18 ← age你把这些字节直接发给另一台机器对方看到的就是一堆无意义的数字地址空间隔离内存里的“mingsec”其实是个指针0x7fff0050这个地址只在当前这个进程里有效你把0x7fff0050发给另一台机器对方的内存地址0x7fff0050上可能是系统内核代码或者别人的银行卡密码一读就崩溃段错误。字节序不同大小端问题你用的是 Intel x86 芯片小端序服务器是 IBM Power 芯片大端序。直接把内存里的二进制发过去年龄 18 会被解读成 301989888。所以需要翻译的过程1.2 转换过程基于以上我们的翻译过程就要解决把指针变成数据值把指针找到的 “mingsec” 字面量直接抄进数据包里。把对象类型变成标签告诉对方这是一个 person 结构体把内存布局变成标准格式XML、JSON、Protocol Buffers。1.3 “可存储/可传输”的格式双方约定好的可“正确还原”数据对象的格式都是“可存储/可传输”的格式。常见的几种格式特点JSON文本格式人类可读体积大Protobuf二进制需要先写 .proto 文件定义结构MessagePack二进制类似 JSON 但更紧凑XML文本格式冗长但结构清晰# 内存中的 person1 对象 ↓ 序列化 {name : mingsec, age:18}2. Java序列化与反序列化面向对象的世界中数据封装为类的属性 对数据进行处理的算法或者说逻辑封装为类的方法程序的结构就是对象的调用。我们面向对象开发当然希望对象可以复用所以需要传输或存储对象。程序运行后是存放在内存中的我们在源代码中new Student(mingsec,7)这个类及对象是JVM在负责管理其在内存中的物理结构可能是散乱在各个物理地址空间的01比特流也可能是一段连续的地址空间。JVM抽象了底层的一切使Java程序员可以以“面向对象”这种逻辑视图进行程序开发。要想将内存中的一个对象发送给另一台机器上的JVM进程使用就需要足够的信息对象的类型是什么类对象的状态属性的值附加信息那么我们就需要定义一个数据格式规定提取出来的对象数据是怎样组织的。与之相对应我们需要两个程序读取类元数据、内存中的对象状态等信息输出一个规范格式字节流/字符流序列化读取一个规范格式的字节流/字符流解析输出一个对象反序列化这是一个很自然而然的过程。我们在本地创建对象首先需要有一个类然后通过new关键字调用构造方法给对象赋值。将一个对象序列化就需要告诉对方类名以及属性的值对方就可以通过读取类名加载类并创建对象读取属性值并给创建的对象赋值。3. Java原生序列化与反序列化关于Java中序列化的格式规范官方文档给出了详细说明也在相应的类中定义了格式标记位。想要深入了解规范追着AI问或者自行阅读官方文档。大致流程如下图Java中的对象想要支持序列化其类需要实现Serialiable接口。这个接口只起到标记作用告诉JVM该类的对象是可序列化的。实现序列化和反序列化的功能分别由java.base模块jdk9里的java.io包下的ObjectOutputStream的writeObject()方法和ObjectInputStream的readObject()方法提供下图给出一个序列化的demo.Java序列化的数据以标记AC ED 00 05开头base64编码的特征为rO0AB。我们看一下官方文档协议格式定义给的类源码2.1 Java原生方法调用链序列化主要方法调用链Java 层面 → JVM 本地方法ObjectOutputStream.writeObject(Objectobj)→writeObject0(Objectobj,booleanunshared)→writeOrdinaryObject(Objectobj,ObjectStreamClassdesc,booleanunshared)→writeSerialData(Objectobj,ObjectStreamClassdesc)→DataOutputStream.writeInt(intval)→JVM_WriteInt(C/C实现)// 本地方法写入基本类型反序列化主要方法调用链Java 层面 → JVM 本地方法ObjectInputStream.readObject()→readObject0(booleanunshared)→readOrdinaryObject(booleanunshared)→ObjectStreamClass.newInstance()→JVM_NewInstanceFromConstructor(C/C实现)// 本地方法创建对象→readSerialData(Objectobj,ObjectStreamClassdesc)→Field.set(Objectobj,Objectvalue)→JVM_SetField(C/C实现)// 本地方法设置字段值2.2 自定义writeObject()和readObject()Java支持在类中自定义readObject和writeObject允许开发者细粒度控制序列化和反序列化过程。为什么自定义的 readObject() 或writeObject() 方法会被调用Java 序列化机制的约定Java 的序列化机制在反序列化时会检查目标类是否定义了以下方法privatevoidreadObject(ObjectInputStreamin)throwsIOException,ClassNotFoundException如果该方法存在Java 会通过反射机制调用该方法而不是使用默认的反序列化逻辑。这是 Java 序列化机制的一个约定用于支持类级别的自定义反序列化逻辑。反射机制的调用Java 的 ObjectInputStream 在反序列化一个对象时会执行以下步骤检查目标类是否有自定义的 readObject() 方法。如果有通过反射调用该方法。如果没有则使用默认的反序列化逻辑即逐个读取字段值并赋值给对象。这种机制是通过 ObjectStreamClass 类实现的。ObjectStreamClass 会缓存类的序列化方法信息并在反序列化时调用相应的 invokeReadObject() 方法。4. 总结我们可以把序列化/反序列化拆成两个正交的维度来看第一维硬件底层的“语法序列化”——解决“比特怎么看”负责内容字节序大端/小端、内存对齐Padding、浮点数标准IEEE 754。发生场景任何跨设备传输哪怕是发送一个 int。高级语言的态度封装。Java/Python 的 JVM/解释器已经帮你把 int 转成了固定大端格式的网络字节流程序员在写 writeInt() 时甚至感觉不到字节序的存在——这就是高级语言的好处。第二维语言语义的“结构序列化”——解决“指针对不对”负责内容引用修复Reference Fixup、对象图循环、多态类型还原。为什么基础类型不需要这步 因为基础类型是值语义Value Semantics它是平的而引用类型是指针语义Pointer Semantics它是有向图。内存中的数据不是裸字节而是带有类型、指针、内存布局等语义的结构。这些语义只有当前运行环境能理解。序列化就是把这些环境相关的结构翻译成通用的、可传输的格式。5. 参考[1] Java序列化格式详解[2] 官方文档Java Object Serialization Specification[3] Java安全小白的入门心得 - java反序列化[4] 通义qwen3[5] deepseek[6] mimo