摘要:Java 8 官方支持已结束,社区也不再维护。继续留在 Java 8 等于裸奔——没安全补丁、没性能优化、用不了虚拟线程和 Record。本文不翻译官方文档,只讲一个 Java 8 老兵的真实升级经历:从 8 直奔 25,踩了什么坑、怎么解的、值不值。顺便告诉你为什么别在 17 或 21 浪费时间——跳一次就够,管到 2033。

背景:Java 8 真的不能再用了

上个月,组里一个新同事问我:「这个项目为什么还在用 Java 8?」

我愣了一下。说实话,不是不想升,是一想到升级的麻烦,就一年拖一年。项目跑了快十年,稳得像块石头。谁愿意动它?

直到有一天,安全团队扫出一堆 CVE——全是 Java 8 的洞,没人修了。

Java 8 是 2014 年发布的。Oracle 的官方支持早就停了,社区支持也在今年彻底终止。换句话说,你的 Java 8 应用,现在连安全补丁都拿不到了

而另一边,同事们的新项目用上了虚拟线程、Record、模式匹配 switch——三十行代码干完我三百行的事。每次 Code Review,我都在关掉那个手写了两百行 getter 的类。

不升不行了。

为什么直接跳到 25,不是 17 也不是 21

决定升级之前,我先把几个 LTS 版本捋了一遍:

版本 发布时间 支持截止 关键卖点
Java 8 2014.03 已终止 你正在用的
Java 17 2021.09 约 2029 封印类、Record 成熟
Java 21 2023.09 约 2031 虚拟线程正式版
Java 25 2025.09 至少 2033 紧凑对象头、18 项 JEP、最新 LTS

算一笔账就清楚了。

方案 A:8 → 17 → 21 → 25。踩三次坑,折腾三轮。每次升级都得处理模块化、废弃 API、第三方依赖兼容性。三次下来,光排查问题就能掉半条命。

方案 B:8 → 25,只跳一次。坑集中踩完,一步到位。Java 25 是 2025 年 9 月发布的 LTS,Oracle 承诺至少支持 8 年——从现在算起能管到 2033 年。下一个 LTS 什么时候出还不一定。从生命周期看,25 是眼下最「长命」的选择。

至于 Java 26(2026 年 3 月发布,非 LTS)和还没定版的 Java 27——非 LTS 版本只支持 6 个月,根本不值得考虑。

结论:别走 17 过渡。坑,一次踩完就够了。

升级实战:从 8 到 25 的四道坎

跨了 9 个大版本,坑是不可避免的。下面这四个是我实打实遇到的。

第一坎:模块化系统(Java 9)

Java 9 引入了 JPMS(Java Platform Module System),最直接的影响就是 sun.* 内部 API 被封了。

编译直接报错:

java.lang.reflect.InaccessibleObjectException:
  Unable to make field private final byte[] java.lang.String.value
  accessible: module java.base does not "opens java.lang" to unnamed module

如果你的项目或依赖里用了反射访问 JDK 内部类,升级后必炸。

解法分两步。先用 JDK 自带的 jdeps 扫一遍:

jdeps --jdk-internals your-app.jar

它会列出所有对 sun.* 等内部 API 的调用。能改的就改掉——大部分场景 JDK 自己已经提供了替代 API。

实在改不了的(比如某些老框架深度依赖),用 --add-opens 临时放行:

java --add-opens java.base/java.lang=ALL-UNNAMED \
     --add-opens java.base/java.util=ALL-UNNAMED \
     -jar your-app.jar

别把这当永久方案。--add-opens 是「先让系统跑起来」的止血措施,后面要逐个清理。

第二坎:被移除的 API(Java 11/17)

从 Java 8 到 25,JDK 删了不少东西。最坑的两个:

JAXB / JAX-WS 被移除。如果你的项目用了 javax.xml.bind.* 做 XML 处理,升级后直接 ClassNotFoundException。

解法:Maven 加独立依赖:

<dependency>
  <groupId>jakarta.xml.bind</groupId>
  <artifactId>jakarta.xml.bind-api</artifactId>
  <version>4.0.2</version>
</dependency>

Nashorn JS 引擎被干掉。如果代码里调了 ScriptEngineManager 跑 JavaScript,得换 GraalJS。

这两个问题不复杂,但排查起来很耗时——它们不会在编译期报错,只会在运行时崩。

第三坎:老库不兼容

这是我们升级中花时间最多的一块。

最大的雷是 fastjson 1.x 不支持 Record 序列化。Java 16 引入的 Record 类,fastjson 1.x 根本不认识,序列化出来是空对象。解法就一条:升到 fastjson2

其他几个常见依赖升级:

  • Lombok:升到 1.18.36+,否则 Java 25 的 class file 版本不认
  • Byte Buddy / CGLIB:各种 AOP 框架底层依赖,务必升到最新
  • Hibernate:至少 6.x,5.x 对 Java 17+ 的兼容性已经很差了

Maven 的 maven-enforcer-plugin 可以帮你检查依赖的 JDK 兼容性,强烈建议先跑一遍。

第四坎:TLS 和 SecurityManager

两个安全相关的变化,很容易被忽略。

TLS 1.0/1.1 默认禁用(Java 17 开始)。如果你的应用连的是老版本 MySQL、Redis 或者其他老服务,SSL 握手直接失败。日志里会看到 SSLHandshakeException

短期解法:修改 java.security 配置文件,把 jdk.tls.disabledAlgorithms 里的 TLSv1 和 TLSv1.1 去掉。长期方案:升级数据库和服务端。

SecurityManager 永久禁用(Java 25)。之前几个版本只是标记 deprecated,Java 25 直接不给用了。启动时如果带了 -Djava.security.manager 参数,进程起不来。如果你的应用没用到 SecurityManager——大多数 Java 8 项目都不会用——这事跟你没关系。但如果之前配过,必须去掉。

踩坑速查表

做升级那两周,我建了个备忘录。后来发现同事也遇到一样的问题,干脆整理成表:

报错信息 原因 解法
InaccessibleObjectException 反射访问内部 API jdeps --jdk-internals 扫描,--add-opens 放行
ClassNotFoundException: javax.xml.bind.* JAXB 被移除 jakarta.xml.bind-api 依赖
ClassNotFoundException: jdk.nashorn.* Nashorn 被移除 换 GraalJS
SSLHandshakeException TLS 1.0/1.1 被禁用 升级数据库/服务端,或临时放行
UnsupportedClassVersionError class file 版本不匹配 升 Lombok / Byte Buddy / ASM
Record 序列化为空 JSON fastjson 1.x 不支持 升到 fastjson2
SecurityManager 启动报错 Java 25 直接禁了 去掉 -Djava.security.manager

迁移工具——OpenRewrite 一把梭

踩完前面那些坑之后,我才发现其实有更好的方式。

OpenRewrite 是一个 Maven 插件,能自动帮你改代码——替换废弃 API、升级依赖版本、迁移到新语法。你跑一下,它能处理掉大约 60-70% 的机械化改动。

配置很简单,在 pom.xml 加一段:

<plugin>
  <groupId>org.openrewrite.maven</groupId>
  <artifactId>rewrite-maven-plugin</artifactId>
  <version>5.43.0</version>
  <configuration>
    <activeRecipes>
      <recipe>org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta</recipe>
      <recipe>org.openrewrite.java.migrate.UpgradeToJava25</recipe>
    </activeRecipes>
  </configuration>
</plugin>

然后跑:

mvn rewrite:run

它会自动扫项目、改代码、生成 diff。改完之后 git diff 看一遍,确认没问题再提交。

三件 OpenRewrite 帮不了你的事:

  1. 业务逻辑层面的调整(比如你针对旧 API 写的 workaround)
  2. 运行时配置(--add-opensjava.security 这些得手配)
  3. 第三方依赖的内部兼容性问题

推荐组合拳:OpenRewrite 扫一遍 → jdeps + jdeprscan 各跑一次 → maven-enforcer-plugin 检查依赖 → 剩下的手动改。

升级后开哪些新特性(按性价比排序)

费这么大劲升上来,不开新特性就亏了。按收益从高到低排:

1. 紧凑对象头(零代码改动)

性价比最高,没有之一。加一个 JVM 参数就生效:

-XX:+UseCompactObjectHeaders

对象头从 12 字节压缩到 8 字节。实测下来,堆内存降 3-5%,CPU 开销最高降 30%。什么都不用改,重启就能吃的性能红利。

2. 虚拟线程(低改动,高收益)

原来用线程池处理并发任务,一百个线程就是一百个平台线程,内存和上下文切换开销都不小。虚拟线程一个平台线程上能挂成千上万个:

var executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> {
    // 处理请求,该阻塞就阻塞,不怕
});

百万级并发不是梦。Java 24 还修了 synchronized 导致的 pinning 问题,到 25 已经相当稳了。

3. Record + 模式匹配(写法爽)

告别手写 getter/setter/toString:

record User(Long id, String name, String email) {}

配合模式匹配 switch:

var result = switch (obj) {
    case User(var id, var name, _) -> "用户: " + name;
    case Order(var orderId, var amount) -> "订单: " + orderId;
    default -> "未知类型";
};

代码量砍一半,可读性翻倍。写惯了之后,回到 Java 8 会觉得自己在写废代码。

4. 其他锦上添花

  • 文本块(Java 15):三引号写 SQL/JSON,不用再拼 + "\n"
  • 向量 API(Java 16-25 正式版):做 AI 和数据处理,SIMD 指令直接上,不用 JNI
  • 密封类(Java 17):限制继承关系,编译器帮你把关

总结

升级这事,越想越怕。真动手了,也就两周。

从 Java 8 直奔 25,踩一次坑,管到 2033。 别走 17 过渡——每多一次过渡就多一轮排查,纯粹浪费时间。

如果你们组还在 Java 8 上,我的建议是:先拿一个非核心服务试水,把上面这些坑趟一遍。跑稳了,再把核心服务搬过来。等你用上虚拟线程和 Record,再回头写 Java 8 代码,会觉得像在用十年前的手机。

参考:

作者:唐悦玮 | 公众号同名

从后端出发,用 AI 拓展到全栈的工程师。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐