在Java开发中,对象序列化(Serialization)是指将内存中的Java对象转换为字节序列的过程,而反序列化(Deserialization)则是将这些字节序列重新构造成内存中的对象。这个机制就像把三维物体压扁成一张图纸(序列化),需要时又能按图纸还原出立体结构(反序列化)。
核心价值与应用场景:
关键提示:序列化后的字节流不包含类的结构信息,反序列化时需要依赖本地class定义
Java通过java.io.Serializable标记接口实现序列化能力。任何需要序列化的类只需实现这个空接口:
java复制public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // 不被序列化的字段
}
关键组件说明:
ObjectOutputStream:序列化操作的核心类ObjectInputStream:反序列化操作的核心类transient关键字:标记不参与序列化的字段serialVersionUID:版本控制标识符当执行ObjectOutputStream.writeObject()时,JVM会:
java复制// 典型序列化代码示例
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.dat"))) {
oos.writeObject(user);
}
Java原生反序列化存在严重安全风险,攻击者可能构造恶意字节流实现:
防护方案:
| 方案 | 序列化大小 | 序列化耗时 | 反序列化耗时 | 语言跨平台 |
|---|---|---|---|---|
| Java原生 | 100% | 100% | 100% | 仅Java |
| JSON(Gson) | 220% | 350% | 400% | 是 |
| Protobuf | 30% | 60% | 70% | 是 |
| Kryo | 25% | 40% | 50% | 有限支持 |
protobuf复制syntax = "proto3";
message User {
string username = 1;
int32 age = 2;
}
bash复制protoc --java_out=. user.proto
java复制UserProto.User user = UserProto.User.newBuilder()
.setUsername("test").setAge(25).build();
byte[] bytes = user.toByteArray(); // 序列化
UserProto.User newUser = UserProto.User.parseFrom(bytes); // 反序列化
Kryo需要特别注意线程安全问题:
java复制// 正确的线程局部使用方式
private static final ThreadLocal<Kryo> kryos = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kryo();
kryo.register(User.class);
return kryo;
});
void serialize(User user) {
Output output = new Output(new FileOutputStream("file.bin"));
kryos.get().writeObject(output, user);
output.close();
}
当类结构变更时,需要处理:
推荐方案:
java复制private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
// 版本迁移逻辑
if(serialVersionUID < CURRENT_VERSION) {
this.newField = "default";
}
}
在Spring Cloud微服务中配置:
yaml复制# 使用Kryo作为Feign的序列化器
feign:
encoder:
kryo:
enabled: true
registrations:
- com.example.User
java复制private static final Pool<Output> outputPool = new Pool<>(true, false, 16) {
protected Output create() {
return new Output(1024, -1);
}
};
void optimizedSerialize(User user) {
Output output = outputPool.obtain();
try {
kryo.writeObject(output, user);
// 使用output.getBuffer()
} finally {
outputPool.free(output);
}
}
java复制kryo.setRegistrationRequired(true);
kryo.register(User.class, 10); // 分配固定ID
| 异常类型 | 原因分析 | 解决方案 |
|---|---|---|
| NotSerializableException | 未实现Serializable接口 | 实现接口或使用transient |
| InvalidClassException | serialVersionUID不匹配 | 显式声明固定版本号 |
| StreamCorruptedException | 字节流被篡改 | 校验数据完整性 |
| EOFException | 数据不完整 | 检查传输过程是否中断 |
反序列化时可能出现的循环引用问题:
java复制// 启用循环引用检测
kryo.setReferences(true);
// 或者使用IdentityMap
kryo.setDefaultSerializer(DefaultSerializers.ObjectSerializer.class);
JSON方案中的日期处理陷阱:
java复制Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss")
.create();
// 必须明确时区处理
TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
新兴方案如Fury通过堆外内存实现:
java复制Fury fury = Fury.builder().build();
byte[] bytes = fury.serialize(obj); // 无需内存拷贝
Avro等方案支持动态Schema:
java复制Schema schema = new Schema.Parser().parse(new File("schema.avsc"));
DatumWriter<User> writer = new SpecificDatumWriter<>(schema);
使用SIMD指令集的序列化器:
java复制// 配置启用SIMD
System.setProperty("arrow.enable_unsafe_memory_access", "true");
在实际项目中,我通常会根据业务特点选择方案:对性能要求极高的核心交易采用Kryo,需要跨语言的场景用Protobuf,调试阶段用JSON。特别要注意的是,任何序列化方案都需要进行严格的压测,我曾经遇到过Protobuf在大对象(超过1MB)时性能急剧下降的情况,后来通过分块序列化解决了这个问题。