Java 25 是今年 9 月 16 号发布的,距今不足 2 月。已经发现了好几个 bug 了。这些 bug 当中,有几个已经修复了。于是,官方陆续的发布了几个对应 JDK 的修复版本,其中针对 Java 25,发布的是 JDK 25.0.1。在这个版本中,有一个影响 JDK 25 重大兼容性陷阱,那就是 java.time 序列化问题。本文将深度解析一下。
前言
JDK 25 带来了很多激动人心的新特性,我也写了好几篇陆续关于 JDK 25 新特性的文章。但在升级过程中,一些开发者们发现在使用过程中可能会遇到一个隐藏的"地雷",即 java.time 包中几个核心类的序列化兼容性问题。这个问题可能导致你的应用在升级后无法正常反序列化数据,甚至抛出 InvalidClassException。这个由 JDK-8367031 记录的 bug 终于被 PR 了。
受影响的核心类
收到影响的几个类,如下所示。
java.time.LocalDatejava.time.YearMonthjava.time.MonthDayjava.time.chrono.HijrahDate
问题的本质
JDK 25 与之前版本之间存在序列化不兼容性,但这里有一个重要的细节:只有 Class 对象的序列化受影响,实例对象的序列化是正常的。
也就是说,升级 JDK 25 之前是好的,升级之后可能就会出现InvalidClassException问题。
技术根因分析
根据官方修复的https://bugs.openjdk.org/browse/JDK-8367031介绍显示,这个兼容性问题的根源可以追溯到 JDK-8334742,该变更是为了支持 Valhalla 项目中的值类(Value Classes)特性。Valhalla 项目旨在为 Java 引入值对象,结合面向对象编程的抽象性和简单原语的性能特征。
字段类型变更
官方为了实现更紧凑的内存布局,JDK 团队将以下字段的类型从 short 改为 byte。
// 变更前的字段类型(JDK 24及之前)
privatefinalshort year;
privatefinalshort month;
privatefinalshort day;
// 变更后的字段类型(JDK 25)
privatefinalint year; // year保持int类型
privatefinalbyte month; // month从short改为byte
privatefinalbyte day; // day从short改为byte

JDK17
这个紧凑的内存布局,短了一字节,也短了命,让 JDK 25 的"字节"级事故坑到了不少老外尝鲜的网友。
为什么实例序列化不受影响?
不得不说,这里面有一个精妙的设计。即 java.time 类使用了序列化代理模式(Serialization Proxy Pattern)。
// LocalDate 的序列化代理实现
private Object writeReplace() {
return new Ser(Ser.LOCAL_DATE_TYPE, this);
}
private Object readResolve() throws ObjectStreamException {
throw new InvalidObjectException("A LocalDate proxy is required");
}
实际的序列化通过 java.time.Ser 类完成,该类实现了 Externalizable 接口,显式地编码和解码日期值,因此不受字段类型变更的影响。
问题示例
我们先看一个会出问题的代码(序列化 Class 对象)。
// 在JDK 24中序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("localdate-class.ser"));
// 下面这个在JDK 25中会出问题
oos.writeObject(LocalDate.class);
oos.close();
// 在JDK 25中反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("localdate-class.ser"));
// 下面的这行代码会抛出 InvalidClassException 异常
Class<?> clazz = (Class<?>) ois.readObject();
ois.close();
下面我们来看一个序列化实例对象正常的代码。
// 在JDK 24中序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("localdate-instance.ser"));
// 这个在任何版本都正常
oos.writeObject(LocalDate.now());
oos.close();
// 在JDK 25中反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("localdate-instance.ser"));
// 完美兼容
LocalDate date = (LocalDate) ois.readObject();
ois.close();
异常详情
当问题发生时,日志中可能会看到如下异常:
java.io.InvalidClassException: java.time.LocalDate; incompatible types for field day
at java.base/java.io.ObjectStreamClass.matchFields(ObjectStreamClass.java:2077)
at java.base/java.io.ObjectStreamClass.checkClass(ObjectStreamClass.java:2049)
at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:908)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2335)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:2208)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2218)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1768)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:547)
下面我们说一下官方的修复过程。
解决方案和修复过程
官方解决方案
经过社区反馈,OpenJDK 团队意识到了这个兼容性问题的严重性,决定回退 JDK-8334742 的变更,恢复受影响类的原始字段类型。
你看,官方评估过后,考虑到严谨性,首选的方案竟然是回退版本。要是国内企业,更可能是求快,赶鸭子上架,草草的改一下继续上线。
修复的关键点
毕竟,退回还是最快最可靠最简单的修复方案了。后面我感觉还会继续改为 byte 类型,但具体时间未定,也可能在 JDK 26、27 中再现。
- 完全回退字段类型变更:将
byte恢复为short - 保持 Valhalla 项目的兼容性:寻找其他方式支持值类特性
- 加强序列化兼容性测试:增加跨版本的序列化测试用例
临时解决方案
在官方注意到这个 bug 后,也有临时的解决方案。即,在修复发布之前,如果大家的应用受到此问题影响,可以采取下面 3 中临时方案。
- 避免序列化 Class 对象:改为序列化实例对象
- 使用自定义序列化:实现自己的序列化机制
- 保持版本一致:确保序列化和反序列化使用相同的 JDK 版本
受影响的应用场景
根据老外网友的讨论,了解到目前有一下一些框架受到影响。
- 分布式缓存系统(如 Hazelcast、Apache Ignite)
- RPC 框架中的类信息传输
- 序列化框架(如 Kryo、FST)的兼容性层
- 某些特定的反射工具库
最佳实践建议
尽量最小化颗粒度的序列化,以及避免序列化 Class 对象。
// 推荐:序列化具体的数据对象
public class DateEvent implements Serializable {
private LocalDate date; // 序列化实例
private String description;
}
// 避免:序列化Class对象
public class ClassHolder implements Serializable {
private Class<?> dateClass; // 可能引发兼容性问题
}
总结
实际上,JDK 25.0.1 共修复了 5 个 bug。如下图所示。

JDK-8367031 只是其中最显眼的一个罢了。它告诉我们,一个看似简单的字段类型变更,也可能引发意想不到的兼容性问题。
最后,切记,最先吃螃蟹是有风险的,即使在进行 JDK 升级时,也尽量做到充分的测试和评估是避免生产环境问题的关键。
另外JDK 25 作为最新的长期支持(LTS)版本,虽然带来了许多令人兴奋的新特性,但也引入了一些需要特别注意的重大兼容性变化。了解这些变化对于平稳升级至关重要。
下面这个表格汇总了目前已知的主要兼容性问题点,帮助您快速把握全局。
| 问题领域 | 具体表现 | 影响严重度 | 关键信息 |
|---|---|---|---|
| 架构支持 | 移除了对 32 位 x86 架构的支持 | 高(针对特定环境) | 无法在 32 位 Linux x86 系统上运行 JDK 25 |
| 线程本地数据共享 | 引入 Scoped Values作为 ThreadLocal的现代替代方案 |
中(长期影响) | 不立即破坏兼容性 ,但预示 ThreadLocal的未来使用方式变化 |
| 模块系统 | 新增 import module语法,可能引发命名冲突 |
低至中 | 若同时导入不同模块中的同名类,需显式指定以消除歧义 |
| 构造函数语法 | 允许在 super()或 this()调用前执行代码 |
低(行为变化) | 改变了传统的构造函数执行模型,可能影响依赖此顺序的复杂继承结构 |