2026 年 9 月Oracle JDK 8 的 NFTC免费使用条款将到期。而 Oracle JDK 8 的公共更新早在 2019 年 1 月就已停止。不管从哪个时间点看还在 JDK 8 上的企业都不得不面对一个现实迁移到 21 不是要不要的问题而是什么时候做的问题。但 JDK 8 到 21中间隔了 13 个版本、10 年的语言演进。一个中型项目几百万行代码纯手工迁移测试排期、兼容性问题、行为变更——想想就头大。所以很自然的一个问题大模型能不能帮上忙我用一个真实的 Java 8 项目做了实验让大模型辅助做 JDK 21 迁移。下面是完整的过程和结果。迁移的核心工作量JDK 8 → 21 的迁移工作量大致分三层第一层语法升级简单可自动化• 匿名内部类 → Lambda 表达式• 传统 for 循环 → Stream API• 字符串拼接 → Text Blocks• switch 语句 → switch 表达式• 普通类 → Records•Optional.isPresent() get()→ifPresent()/orElse()第二层API 替换中等大部分能自动•java.util.Date/Calendar→java.time.LocalDateTime/Instant• JAXB 依赖外置javax.xml.bind在 JDK 11 被移除需添加外部依赖•javax→jakarta命名空间迁移Jakarta EE 9 的包名变更与 JDK 升级配套处理第三层行为变更高风险需要人工判断• 虚拟线程兼容性synchronized在虚拟线程中的 pinning 问题JDK 25 已修复•ThreadLocal在虚拟线程下的表现变化• GC 参数变化CMS 已被移除需要切换到 G1/ZGC• 某些sun.misc包下的内部 API 已被移除• 模块系统JPMS强封装反射访问受限需要--add-opens/--add-exports• Security Manager 废弃与禁用JDK 17 标记废弃JDK 24 永久禁用大模型在第一层做得很好第二层做得还行第三层经常翻车。下面具体看。第一层语法升级——AI 的舒适区我喂给 Claude Code 一段典型的 JDK 8 代码// 原始 JDK 8 代码public class OrderProcessor {public ListString getActiveUserEmails(ListUser users) {ListString emails new ArrayList();for (User user : users) {if (user.getStatus() UserStatus.ACTIVE) {String email user.getEmail();if (email ! null !email.isEmpty()) {emails.add(email.toLowerCase());}}}Collections.sort(emails);return emails;}public OptionalUser findUser(ListUser users, Long id) {for (User user : users) {if (user.getId().equals(id)) {return Optional.of(user);}}return Optional.empty();}public String formatReport(String title, ListString items) {StringBuilder sb new StringBuilder();sb.append( title \n);for (int i 0; i items.size(); i) {sb.append((i 1) . items.get(i) \n);}return sb.toString();}}AI 输出// AI 迁移后的 JDK 21 代码public class OrderProcessor {public ListString getActiveUserEmails(ListUser users) {return users.stream().filter(u - u.getStatus() UserStatus.ACTIVE).map(User::getEmail).filter(email - email ! null !email.isEmpty()).map(String::toLowerCase).sorted().toList(); // JDK 16 直接返回不可变 List}public OptionalUser findUser(ListUser users, Long id) {return users.stream().filter(u - u.getId().equals(id)).findFirst();}public String formatReport(String title, ListString items) {return STR. \{title} \{IntStream.range(0, items.size()).mapToObj(i - STR.\{i 1}. \{items.get(i)}).collect(Collectors.joining(\n))};}}评价令人满意。Stream API、方法引用、toList()、Text Blocks——AI 全部正确应用。唯一要注意的是字符串模板STR....。这个特性在 JDK 21 中只是首次预览JEP 459JDK 22 进入第二次预览JDK 23 被撤回至今仍未成为正式特性。所以这段代码在 JDK 21 上必须加--enable-preview才能运行而且即使能跑API 在后续版本中也发生了变化。稳妥的做法是先用 Text Blocks 替代字符串拼接字符串模板等它正式定型再迁移。第一层迁移AI 几乎是完美的。手动做的话这些代码可能要改 20 分钟AI 20 秒搞定。第二层API 替换——有好有坏第二层就有点意思了。我给了 AI 一段包含java.util.Date和SimpleDateFormat的代码// 原始 JDK 8 代码public Date parseDate(String dateStr) {SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);try {return sdf.parse(dateStr);} catch (ParseException e) {throw new RuntimeException(日期解析失败: dateStr, e);}}public long daysBetween(Date start, Date end) {long diff end.getTime() - start.getTime();return TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);}AI 迁移为// AI 迁移后的代码public LocalDateTime parseDate(String dateStr) {DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss);try {return LocalDateTime.parse(dateStr, formatter);} catch (DateTimeParseException e) {throw new RuntimeException(日期解析失败: dateStr, e);}}public long daysBetween(LocalDateTime start, LocalDateTime end) {return Duration.between(start, end).toDays();}评价语法正确但要注意返回值变更。Date→LocalDateTime本身没问题。但调用方如果之前拿Date去和java.sql.Date交互或者用了Date的after()/before()方法迁移后需要联动修改。这是 AI 看不到的——它只改了当前文件不知道调用方怎么用这个返回值。这就是第二层迁移的真正痛点API 替换不是简单的搜索替换它涉及接口契约的变化。如果你返回类型从Date变成了LocalDateTime所有调用方都得改。AI 迁移第二层的安全做法是告诉它保留公开方法的签名不变内部实现改为 java.time。// 更好的迁移保持签名改内部实现public Date parseDate(String dateStr) {DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss);LocalDateTime ldt LocalDateTime.parse(dateStr, formatter);return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());}这样减少连锁改动可以分批迁移。另外还有一个容易忽略的边界问题Duration.between(LocalDateTime, LocalDateTime).toDays()和原来的TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS)在大多数情况下结果一致但LocalDateTime不感知时区和 DST夏令时切换。如果原始代码涉及跨时区或夏令时的日期计算迁移后需要额外验证。第三层行为变更——AI 翻车的地方第三层涉及 JDK 内部行为的变化AI 的表现开始不稳定。我给它一段使用了ThreadLocal的代码// 原始代码public class RequestContextFilter implements Filter {private static final ThreadLocalRequestContext contextHolder new ThreadLocal();Overridepublic void doFilter(ServletRequest request, ...) {RequestContext ctx buildContext(request);contextHolder.set(ctx);try {chain.doFilter(request, response);} finally {contextHolder.remove();}}public static RequestContext getContext() {return contextHolder.get();}}我问 AI这段代码迁移到 JDK 21 后在虚拟线程环境下有什么问题AI 的回答这段代码在虚拟线程下可能有问题因为虚拟线程是 JVM 管理的协作式调度ThreadLocal 在虚拟线程中的行为与传统线程一致但在使用虚拟线程池时需要注意 ThreadLocal 的清理。这个回答只对了一半。真实的问题是虚拟线程支持数以万计的并发但 ThreadLocal 的内存开销是按线程算的。如果用传统线程池如 Tomcat 的 200 线程池200 个 ThreadLocal 实例可以接受。但如果用虚拟线程可以创建 10 万个ThreadLocal 的内存和 GC 压力会急剧上升。ThreadLocal 在虚拟线程中仍然可用但建议改用ScopedValueJDK 20预览。AI 没有主动提到ScopedValue也没有量化 ThreadLocal 在大量虚拟线程下的内存问题。如果你完全相信 AI 的迁移建议上线后可能会遇到意外的性能问题。第三层的 AI 输出只能作为提醒不能作为决策依据。实操建议AI 辅助迁移的工作流这次实验下来我觉得最优的迁移方式不是让 AI 全量迁移也不是完全不用 AI而是这样第一步AI 做语法扫描用 AI 扫描整个项目定位需要迁移的位置输出一个清单需要升级的语法模式- 匿名内部类 → Lambda237 处- for 循环 → Stream108 处- switch 语句 → switch 表达式45 处- 字符串拼接 → Text Blocks62 处需要替换的 API- java.util.Date89 处- SimpleDateFormat34 处- javax.xml.bind12 处- ThreadLocal16 处需检查虚拟线程兼容性这个清单帮你评估工作量而不是猜。第二步AI 做批量语法迁移第一层语法升级可以直接让 AI 批量处理。用脚本或 AI 工具对整个模块做一次性替换然后走代码审查。注意每次只提交一个包或一个模块的变更不要一个 PR 改几百个文件。第三步API 替换按模块推进第二层的 API 替换建议按模块推进。每个模块在独立的分支上做迁移做完后充分测试再合并到主分支。第四步行为变更做专项审查第三层的问题需要人工列一个清单逐项检查• 是否有synchronized在可能被虚拟线程调用的路径上JDK 25 已修复 pinning但 JDK 21 仍需关注• 是否有 CMS GC 参数需要替换为 G1/ZGC• 是否有sun.misc.*的调用• 是否有反射相关的setAccessible需要调整是否需要添加--add-opens/--add-exportsJVM 参数• ThreadLocal 的使用是否需要替换为 ScopedValue• 是否有SecurityManager的使用JDK 24 已永久禁用把这些交给 AI 去查让它在代码库里辅助定位这些风险点再做人工决策。附迁移工具链除了大模型JDK 迁移还有一些专门工具值得用上工具用途说明jdeps分析模块依赖关系JDK 自带扫描代码对内部 API 的依赖jdeprscan扫描已废弃 API 的使用JDK 自带定位需要替换的废弃方法IntelliJ IDEA 迁移助手IDE 内置检测升级 JDK 版本后自动提示不兼容的代码OpenJDK Migration Guide官方迁移指南每个版本的 JDK Release Notes 都有 Migration 章节Claude Code / CursorAI 辅助批量迁移适合第一层语法升级和第二层 API 替换建议的组合方式先用jdeprscanjdeps做静态扫描拿到风险清单再用 AI 工具做批量语法迁移最后人工审查第三层行为变更。总结迁移层次AI 表现是否需要人工语法升级优秀走代码审查即可API 替换良好需要检查接口兼容性行为变更不稳定必须人工判断JDK 8→21 迁移这件事AI 最好的角色是高级助手而不是主力。它能显著加速第一层和第二层让你从这个项目迁移要三个月变成语法升级两周搞定剩下时间做兼容测试。最难的部分从来不是改代码而是确保改了之后系统还能正常跑。所以别让 AI 全自动跑一个几百万行的项目。找个模块让它处理第一层和第二层你跑一遍测试看看效果——这样逐步建立信任再铺开到整个项目
AI助力JDK8到21迁移实战
2026 年 9 月Oracle JDK 8 的 NFTC免费使用条款将到期。而 Oracle JDK 8 的公共更新早在 2019 年 1 月就已停止。不管从哪个时间点看还在 JDK 8 上的企业都不得不面对一个现实迁移到 21 不是要不要的问题而是什么时候做的问题。但 JDK 8 到 21中间隔了 13 个版本、10 年的语言演进。一个中型项目几百万行代码纯手工迁移测试排期、兼容性问题、行为变更——想想就头大。所以很自然的一个问题大模型能不能帮上忙我用一个真实的 Java 8 项目做了实验让大模型辅助做 JDK 21 迁移。下面是完整的过程和结果。迁移的核心工作量JDK 8 → 21 的迁移工作量大致分三层第一层语法升级简单可自动化• 匿名内部类 → Lambda 表达式• 传统 for 循环 → Stream API• 字符串拼接 → Text Blocks• switch 语句 → switch 表达式• 普通类 → Records•Optional.isPresent() get()→ifPresent()/orElse()第二层API 替换中等大部分能自动•java.util.Date/Calendar→java.time.LocalDateTime/Instant• JAXB 依赖外置javax.xml.bind在 JDK 11 被移除需添加外部依赖•javax→jakarta命名空间迁移Jakarta EE 9 的包名变更与 JDK 升级配套处理第三层行为变更高风险需要人工判断• 虚拟线程兼容性synchronized在虚拟线程中的 pinning 问题JDK 25 已修复•ThreadLocal在虚拟线程下的表现变化• GC 参数变化CMS 已被移除需要切换到 G1/ZGC• 某些sun.misc包下的内部 API 已被移除• 模块系统JPMS强封装反射访问受限需要--add-opens/--add-exports• Security Manager 废弃与禁用JDK 17 标记废弃JDK 24 永久禁用大模型在第一层做得很好第二层做得还行第三层经常翻车。下面具体看。第一层语法升级——AI 的舒适区我喂给 Claude Code 一段典型的 JDK 8 代码// 原始 JDK 8 代码public class OrderProcessor {public ListString getActiveUserEmails(ListUser users) {ListString emails new ArrayList();for (User user : users) {if (user.getStatus() UserStatus.ACTIVE) {String email user.getEmail();if (email ! null !email.isEmpty()) {emails.add(email.toLowerCase());}}}Collections.sort(emails);return emails;}public OptionalUser findUser(ListUser users, Long id) {for (User user : users) {if (user.getId().equals(id)) {return Optional.of(user);}}return Optional.empty();}public String formatReport(String title, ListString items) {StringBuilder sb new StringBuilder();sb.append( title \n);for (int i 0; i items.size(); i) {sb.append((i 1) . items.get(i) \n);}return sb.toString();}}AI 输出// AI 迁移后的 JDK 21 代码public class OrderProcessor {public ListString getActiveUserEmails(ListUser users) {return users.stream().filter(u - u.getStatus() UserStatus.ACTIVE).map(User::getEmail).filter(email - email ! null !email.isEmpty()).map(String::toLowerCase).sorted().toList(); // JDK 16 直接返回不可变 List}public OptionalUser findUser(ListUser users, Long id) {return users.stream().filter(u - u.getId().equals(id)).findFirst();}public String formatReport(String title, ListString items) {return STR. \{title} \{IntStream.range(0, items.size()).mapToObj(i - STR.\{i 1}. \{items.get(i)}).collect(Collectors.joining(\n))};}}评价令人满意。Stream API、方法引用、toList()、Text Blocks——AI 全部正确应用。唯一要注意的是字符串模板STR....。这个特性在 JDK 21 中只是首次预览JEP 459JDK 22 进入第二次预览JDK 23 被撤回至今仍未成为正式特性。所以这段代码在 JDK 21 上必须加--enable-preview才能运行而且即使能跑API 在后续版本中也发生了变化。稳妥的做法是先用 Text Blocks 替代字符串拼接字符串模板等它正式定型再迁移。第一层迁移AI 几乎是完美的。手动做的话这些代码可能要改 20 分钟AI 20 秒搞定。第二层API 替换——有好有坏第二层就有点意思了。我给了 AI 一段包含java.util.Date和SimpleDateFormat的代码// 原始 JDK 8 代码public Date parseDate(String dateStr) {SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);try {return sdf.parse(dateStr);} catch (ParseException e) {throw new RuntimeException(日期解析失败: dateStr, e);}}public long daysBetween(Date start, Date end) {long diff end.getTime() - start.getTime();return TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);}AI 迁移为// AI 迁移后的代码public LocalDateTime parseDate(String dateStr) {DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss);try {return LocalDateTime.parse(dateStr, formatter);} catch (DateTimeParseException e) {throw new RuntimeException(日期解析失败: dateStr, e);}}public long daysBetween(LocalDateTime start, LocalDateTime end) {return Duration.between(start, end).toDays();}评价语法正确但要注意返回值变更。Date→LocalDateTime本身没问题。但调用方如果之前拿Date去和java.sql.Date交互或者用了Date的after()/before()方法迁移后需要联动修改。这是 AI 看不到的——它只改了当前文件不知道调用方怎么用这个返回值。这就是第二层迁移的真正痛点API 替换不是简单的搜索替换它涉及接口契约的变化。如果你返回类型从Date变成了LocalDateTime所有调用方都得改。AI 迁移第二层的安全做法是告诉它保留公开方法的签名不变内部实现改为 java.time。// 更好的迁移保持签名改内部实现public Date parseDate(String dateStr) {DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss);LocalDateTime ldt LocalDateTime.parse(dateStr, formatter);return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());}这样减少连锁改动可以分批迁移。另外还有一个容易忽略的边界问题Duration.between(LocalDateTime, LocalDateTime).toDays()和原来的TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS)在大多数情况下结果一致但LocalDateTime不感知时区和 DST夏令时切换。如果原始代码涉及跨时区或夏令时的日期计算迁移后需要额外验证。第三层行为变更——AI 翻车的地方第三层涉及 JDK 内部行为的变化AI 的表现开始不稳定。我给它一段使用了ThreadLocal的代码// 原始代码public class RequestContextFilter implements Filter {private static final ThreadLocalRequestContext contextHolder new ThreadLocal();Overridepublic void doFilter(ServletRequest request, ...) {RequestContext ctx buildContext(request);contextHolder.set(ctx);try {chain.doFilter(request, response);} finally {contextHolder.remove();}}public static RequestContext getContext() {return contextHolder.get();}}我问 AI这段代码迁移到 JDK 21 后在虚拟线程环境下有什么问题AI 的回答这段代码在虚拟线程下可能有问题因为虚拟线程是 JVM 管理的协作式调度ThreadLocal 在虚拟线程中的行为与传统线程一致但在使用虚拟线程池时需要注意 ThreadLocal 的清理。这个回答只对了一半。真实的问题是虚拟线程支持数以万计的并发但 ThreadLocal 的内存开销是按线程算的。如果用传统线程池如 Tomcat 的 200 线程池200 个 ThreadLocal 实例可以接受。但如果用虚拟线程可以创建 10 万个ThreadLocal 的内存和 GC 压力会急剧上升。ThreadLocal 在虚拟线程中仍然可用但建议改用ScopedValueJDK 20预览。AI 没有主动提到ScopedValue也没有量化 ThreadLocal 在大量虚拟线程下的内存问题。如果你完全相信 AI 的迁移建议上线后可能会遇到意外的性能问题。第三层的 AI 输出只能作为提醒不能作为决策依据。实操建议AI 辅助迁移的工作流这次实验下来我觉得最优的迁移方式不是让 AI 全量迁移也不是完全不用 AI而是这样第一步AI 做语法扫描用 AI 扫描整个项目定位需要迁移的位置输出一个清单需要升级的语法模式- 匿名内部类 → Lambda237 处- for 循环 → Stream108 处- switch 语句 → switch 表达式45 处- 字符串拼接 → Text Blocks62 处需要替换的 API- java.util.Date89 处- SimpleDateFormat34 处- javax.xml.bind12 处- ThreadLocal16 处需检查虚拟线程兼容性这个清单帮你评估工作量而不是猜。第二步AI 做批量语法迁移第一层语法升级可以直接让 AI 批量处理。用脚本或 AI 工具对整个模块做一次性替换然后走代码审查。注意每次只提交一个包或一个模块的变更不要一个 PR 改几百个文件。第三步API 替换按模块推进第二层的 API 替换建议按模块推进。每个模块在独立的分支上做迁移做完后充分测试再合并到主分支。第四步行为变更做专项审查第三层的问题需要人工列一个清单逐项检查• 是否有synchronized在可能被虚拟线程调用的路径上JDK 25 已修复 pinning但 JDK 21 仍需关注• 是否有 CMS GC 参数需要替换为 G1/ZGC• 是否有sun.misc.*的调用• 是否有反射相关的setAccessible需要调整是否需要添加--add-opens/--add-exportsJVM 参数• ThreadLocal 的使用是否需要替换为 ScopedValue• 是否有SecurityManager的使用JDK 24 已永久禁用把这些交给 AI 去查让它在代码库里辅助定位这些风险点再做人工决策。附迁移工具链除了大模型JDK 迁移还有一些专门工具值得用上工具用途说明jdeps分析模块依赖关系JDK 自带扫描代码对内部 API 的依赖jdeprscan扫描已废弃 API 的使用JDK 自带定位需要替换的废弃方法IntelliJ IDEA 迁移助手IDE 内置检测升级 JDK 版本后自动提示不兼容的代码OpenJDK Migration Guide官方迁移指南每个版本的 JDK Release Notes 都有 Migration 章节Claude Code / CursorAI 辅助批量迁移适合第一层语法升级和第二层 API 替换建议的组合方式先用jdeprscanjdeps做静态扫描拿到风险清单再用 AI 工具做批量语法迁移最后人工审查第三层行为变更。总结迁移层次AI 表现是否需要人工语法升级优秀走代码审查即可API 替换良好需要检查接口兼容性行为变更不稳定必须人工判断JDK 8→21 迁移这件事AI 最好的角色是高级助手而不是主力。它能显著加速第一层和第二层让你从这个项目迁移要三个月变成语法升级两周搞定剩下时间做兼容测试。最难的部分从来不是改代码而是确保改了之后系统还能正常跑。所以别让 AI 全自动跑一个几百万行的项目。找个模块让它处理第一层和第二层你跑一遍测试看看效果——这样逐步建立信任再铺开到整个项目