侧边栏壁纸
  • 累计撰写 256 篇文章
  • 累计创建 72 个标签
  • 累计收到 6 条评论

目 录CONTENT

文章目录

JDK25已来,为何大多公司仍在JAVA8?

橙序员
2026-01-27 / 0 评论 / 1 点赞 / 14 阅读 / 7,367 字 / 正在检测百度是否收录... 正在检测必应是否收录...
文章摘要(AI生成)

错误,而是系统的行为、性能和稳定性发生了改变。这种变化往往难以追踪和定位,导致团队感到无所适从,最终对升级产生恐惧。这种情况的发生,表明了即使在表面上看似顺利的升级,实际上可能隐藏着深层次的问题。 第四章:真正的风险,不在 JDK,而在你不敢动的那一部分代码。很多时候,企业面对 JDK 升级时,真正的顾虑并不在于 JDK 本身,而在于那些不敢触碰的老代码。这些代码可能是历史遗留,或者是业务核心部分,涉及到复杂的依赖关系和风险。对这些代码的改动往往需要经过严格的测试和验证,使得升级过程变得更加复杂和缓慢。 第五章:真正逼你升级的,从来不是技术本身。技术本身的演进并不会直接推动企业的升级,真正的驱动力往往来自于业务需求、竞争压力或者技术债务的积累。企业在面对市场变化时,才会意识到升级的重要性,从而在技术上做出改变。 第六章:一次相对靠谱的 JDK 升级,应该从哪里开始。进行一次成功的 JDK 升级,建议从小范围的试点开始,逐步评估影响。可以通过建立测试环境、编写测试用例、监控系统行为等方式,来降低升级过程中的风险。同时,也要注重文档和团队的沟通,确保每一个成员都了解升级的目的和过程。 第七章:如果一直不升,会发生什么?不进行 JDK 升级,可能导致技术债务的累积,使得系统逐渐与现代技术脱节,无法利用新的特性和性能优化。同时,还可能面临安全风险,因为旧版本的 JDK 可能不再获得支持和更新。 结语:也许问题不只在我们。在技术升级的过程中,企业文化、团队信心以及对风险的管理同样重要。面对升级的挑战,企业需要在技术和管理上进行双重努力,以适应不断变化的技术生态。

第一章:JDK 25 都发了,为什么大家还在 Java 8

JDK 25 发布那天,我特意去看了一眼发布说明。内容不复杂,新特性不少,语气一如既往地克制,像是在告诉你:
“你可以升级了,但我们不催。”

这种感觉我在 Java 世界里已经很熟了。

同一天,Python 社区的画风完全不一样。Python 3.13 的兼容性讨论、弃用警告、生态适配进度,被反复拿出来说。很多库会直接写在 README 里:“Python 3.8 即将停止支持,请尽快升级。”Java 这边没有这种集体施压。JDK 25 发布了,但 JDK 8 依然能跑、能用、能上线

我翻了下手头几个线上系统的运行环境,结果并不意外:

  • 老核心系统:Java 8
  • 偏边缘的新服务:Java 11
  • 真正用到 17 的,只有少数新项目
  • 至于 21、25,基本只存在于 PPT 和技术分享里

这不是个别现象。招聘网站、云厂商镜像、监控 SDK 默认支持版本,几乎都在默默告诉你一件事:Java 8 依然是“安全版本”(你发任你发,我用java8)。这和 Python 的升级节奏形成了非常明显的反差。

Python 2 → 3,是一次不升级就活不下去的断代。Java 8 → 25,更像是一次你可以一直不动的演进。

从技术角度看,Java 明明一直在进化:

  • 语言层面:var、record、sealed class
  • JVM 层面:GC、JIT、内存模型
  • 工程层面:模块化、工具链

但这些变化,没有哪一项是“非升不可”。

我见过不少 Java 服务,代码风格停在 2016 年,但稳定运行到今天。也见过 Python 项目,因为一个依赖不再支持旧版本,被迫整体升级。

这两种生态的差异,很早就写在设计选择里了。

Java 的向后兼容是它的优势。但到了 JDK 25 这个时间点,这个优势开始变得有点微妙。

因为问题已经不是:

JDK 8 能不能用?

而变成了:

如果一直停在 JDK 8,到底是在保守,还是在逃避某些成本?

这个问题,在技术会议上很少被正面讨论。更多时候,它会被一句话带过:

“先别动,风险太大。”

可风险到底在哪?为什么 Python 升级时大家骂归骂,还是会跟着走;而 Java 这边,哪怕官方已经跑到 25,企业却依然集体停在 8?

我后来发现,真正卡住升级的,从来不是新特性本身。而是升级这件事,一旦开始,就很难只停在“换个 JDK”上。但这件事,只有在你真的尝试过一次升级之后,才会意识到。


第二章:升级 JDK,看起来向下兼容,实际上并不“平滑”

很多人对 Java 升级的第一判断,来自一个几乎写进 DNA 的认知:

Java 是强向下兼容的语言。

这句话本身没错,也是大多数人从jdk7到jdk8无缝升级的真实感受。但问题在于,大多数人只把它理解成了语法层面

你用 Java 8 写的代码,放到 JDK 17、21、25 上,大概率还能编译。fortry-catchStreamlambda,一个都不会少。这也是为什么很多升级评估一开始都显得非常乐观。真正的问题是 Java 的“向下兼容”,从来不等于 JVM 的平滑迁移

第一次认真推进 JDK 升级时,我们的目标设得非常保守:不引入新语法、不改业务逻辑、不升级框架,只把运行时从 Java 8 换成 17。理论依据也很充分:代码是向下兼容的,JVM 只要能跑就行。

结果第一个暴露问题的,不是业务代码,而是 JVM 本身。

从 JDK 9 开始,Java 做了一次非常激进、但长期看又必须要做的事情:模块化(JPMS)。这一步,本质上是在重塑 JVM 的边界。在 Java 8 之前,JDK 更像是一个“开放的整体”。JDK 自己的内部实现,和应用代码之间,并没有严格的隔离。于是很多框架、工具、甚至业务代码,都默认了一件事:

JVM 内部的类,我是可以摸得到的。

比如反射。

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);

在 Java 8,这是一个非常常见、甚至被大量框架依赖的操作。但在模块化之后,这种行为被明确标记为:非法访问(Illegal Reflective Access)。升级后,日志里开始出现大量这样的提示:

Illegal reflective access by xxx

这类 warning 很容易被误判成“噪音”。因为程序还能跑,接口也没挂。但实际上,这不是 JVM 在提醒你“写得不优雅”,而是在明确告诉你:

你现在还能用,是 JVM 在帮你兜底。

于是有人会加启动参数:

--add-opens java.base/java.lang=ALL-UNNAMED

问题是,从这一刻开始,所谓的“向下兼容”已经被你亲手打破了。你不再是被 JVM 兼容,而是用参数强行绕过 JVM 的设计边界。这也是 Java 升级过程中一个非常隐蔽的转折点:

  • 代码层面看起来没变
  • 启动参数开始越来越复杂
  • JVM 行为开始依赖“约定俗成的补丁”

而这一步,一旦走出去,基本就退不回去了。更麻烦的是,这种不平滑迁移,并不是“偶发问题”,而是 Java 设计演进的必然结果。模块化不是可选项,它是为了:

  • 限制 JVM 内部 API 滥用
  • 提升安全性
  • 为长期演进留空间

但代价是:大量在 Java 8 时代“合理存在”的用法,在新 JVM 下被系统性否定了。这也是为什么很多团队会有一种强烈的错觉:

代码明明没变,怎么升级 JDK 反而问题一堆?

因为你真正升级的,不只是一个版本号,而是 JVM 对“什么是合法行为”的判断标准。而这类问题,偏偏又很难在测试环境一次性暴露完。有的库只在特定路径触发反射;有的异常只在高并发下出现;有的 warning 今天是 warning,下一版就变成 error…

这也是 Java 升级和 Python 最大的不同。

Python 的升级是显式断代:你升级,就必须改代码。

Java 的升级是隐式收紧:你不改代码,但 JVM 会慢慢不再纵容你。

这种“看起来兼容,实际上在变”的特性,让 Java 在企业环境里变得越来越尾大不掉。不是升不了,而是你永远无法确定:

下一步,是不是会踩到一个你完全没预期过的 JVM 行为变化?

也正因为这样,很多公司最终选择了一个看似稳妥、但风险被推迟的方案:停在 Java 8。


第三章:真正让升级失败的,不是编译错误,而是线上行为变了

如果只是编译报错,JDK 升级反而简单。

编不过,改代码;启动不了,补参数;问题是可定位的,也是可回滚的。

真正让团队对升级产生恐惧的,往往发生在上线之后。升级前,所有检查都过了:

  • 单元测试全绿
  • 接口回归没问题
  • 压测 QPS、RT 都在预期范围内

代码一行没改,JDK 从 8 换成 17。

上线当天没有事故。第二天开始,监控里出现了一些非常微妙的变化。不是报错,也不是性能雪崩。而是一些**“看起来不该变的行为,变了”**。

最早被发现的是 GC 行为。Java 8 默认用的是 Parallel GC,而 JDK 17 的默认已经变成了 G1。当时的判断很简单:G1 是“更先进的 GC”,不应该比旧的差。

但线上数据并不这么配合。

  • Full GC 次数少了
  • Minor GC 次数变多
  • 单次停顿更短,但更频繁

这对 JVM 来说是“健康变化”,但对业务来说,结果是:

某些接口的 P99 响应时间开始抖动

不是慢,而是不稳定。问题在于,这类变化不会在压测里明显暴露。压测关注的是吞吐和平均值,而不是长尾。你只能在真实流量下,才会看到这些边缘效应。

紧接着出现的是更难定位的问题:类加载行为的变化。JDK 9 之后,类加载和模块边界被重新梳理过。很多“以前恰好能工作”的加载顺序,在新 JVM 下变了。

最典型的是 SPI 机制。

ServiceLoader.load(SomeService.class)

在 Java 8 下,这段代码的加载顺序是稳定的。在新 JDK 下,如果存在多个实现,顺序可能发生变化。大多数时候,这没什么影响。但如果你的代码里隐式依赖了加载顺序,问题就来了:比如默认实现被换了;没有异常,没有日志,只是业务行为“和以前不太一样”。这类问题,几乎不可能靠自动化测试完全覆盖。因为测试本身,也是在“旧认知”下设计的。

还有一类更隐蔽的变化,来自于 JIT。JVM 在新版本里持续优化编译策略。某些代码路径,在 Java 8 下是“冷路径”,在新 JDK 下被识别成“热点”。结果是:

  • 以前不明显的锁竞争,被放大
  • 原本可以忽略的对象创建,开始影响 GC

代码没变,但 JVM 对代码的“理解方式”变了。

这也是为什么很多线上问题,在排查时会陷入一种诡异的状态:

SQL 没变,代码没变,配置没变,只有 JDK 变了

而你又很难证明:问题真的就是 JDK 引起的

到这一步,升级已经不再是技术选型问题了。它变成了一个心理问题。

团队开始本能地回避这种“不可解释风险”。即便你知道:

  • 这些问题不是 JDK 的 bug
  • 而是历史代码对 JVM 行为的过度依赖

但现实是,线上系统不接受“技术上合理”的解释。这也是很多公司在第一次升级尝试之后,迅速得出结论的原因:

不是升不了,
而是不值得再为这种不确定性买单

于是升级计划被无限期搁置。Java 8 继续稳定运行,问题被推迟,而不是被解决。


第四章:真正的风险,不在 JDK,而在你不敢动的那一部分代码

当升级卡在第三章那些“行为变化”上时,团队往往会得出一个结论:

问题太散了,风险不可控。

但后来复盘发现,真正不可控的,从来不是 JDK,而是我们不敢去验证的那一块代码。几乎每个中大型 Java 项目里,都有这样一层东西:

  • 没人愿意动
  • 但所有人都在用
  • 出问题只能回滚

它可能是十年前写的公共组件,也可能是一次紧急需求里硬塞进去的工具类。

在 Java 8 时代,这类代码有一个共同特征:它们和 JVM 的关系非常近。比如自定义 ClassLoader。

public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) {
        // 从非标准路径加载字节码
    }
}

在 Java 8 下,这种实现非常常见。升级之后,问题不一定立刻出现。但一旦涉及模块、服务加载或反射,行为就开始变得不可预测。

再比如字节码增强。无论是早期的 cglib,还是基于 ASM 的工具,很多实现都默认了:

  • 某些 JDK 内部类是存在的
  • 某些方法签名是稳定的

这些假设,在新 JDK 下不再成立。更现实的问题是:这些代码往往没有完整测试。因为它们本来就不是“业务逻辑”。它们被视为基础设施,
被默认是“不会出问题的”。升级 JDK 时,测试覆盖率看起来还不错。但真正和 JVM 行为强相关的部分,几乎没有被验证过。

于是升级就进入了一个死循环:

  • 不敢上线,是因为没验证
  • 不验证,是因为不敢动
  • 不动,就永远无法升级

这也是 Java 升级和其他语言很不一样的地方。Python 项目里,底层行为大多由解释器和库兜住。Java 项目里,很多“工程能力”是直接构建在 JVM 之上的。而这些能力,恰恰是最难平滑迁移的。

还有一个被严重低估的因素,是运维和排障成本。Java 8 的排障手段,大家已经非常熟悉:

  • jmap
  • jstack
  • 老一套 GC 日志

新 JDK 不是不能用这些工具,而是行为、参数、输出都在变化。同一条 GC 日志,在不同版本下,含义已经不完全一致。这会直接导致一个现实问题:

出问题时,团队是否有信心“看懂”新 JDK 的行为?

如果答案是否定的,那升级本身就是一种冒险。

于是你会看到一种很典型的现象:

  • 开发知道 Java 17 更好
  • 架构知道 Java 21 是趋势
  • 但一到生产,所有人都默认:还是 Java 8 吧

不是因为它完美,而是因为它足够“熟”。

升级 JDK,本质上不是技术债的清理,而是一次对未知的正面接触。而大多数系统,并没有为这种接触做好准备。也正因为这样,很多公司并不是“卡在 Java 8”,而是被 Java 8 保护了很多年。


第五章:真正逼你升级的,从来不是技术本身

在很多公司里,JDK 升级从来不是一个“主动议题”。它通常出现在某个非常具体、而且很现实的场景里。比如云厂商的一封邮件。内容往往写得很克制,大概意思是:

某某 JDK 版本即将停止安全更新
请尽快规划升级方案

这类邮件第一次看到时,大多数人并不会紧张。因为“即将”往往意味着还有一段缓冲期。真正产生压力的,是第二封、第三封。

当你发现云厂商的默认镜像开始发生变化,新建实例已经不再提供 Java 8,升级这件事,就从“技术选择”变成了外部约束。还有安全审计。Java 8 的漏洞,并不比新版本多。但问题在于:很多漏洞,在 Java 8 上不再修了。这意味着同样一个问题:

  • 在新 JDK 上,是一个补丁
  • 在 Java 8 上,是一个长期风险

安全团队不会和你讨论 JVM 设计演进。他们只看结果:
有没有官方支持,有没有风险背书

接着是第三方生态。越来越多的中间件、SDK、监控工具,开始把“最低支持 JDK”往上抬。不是突然抛弃 Java 8,而是新功能不再考虑它。

你会慢慢发现:

  • 想用新版本框架 → 需要新 JDK
  • 想接入新工具 → 官方不再测试 Java 8
  • 想拿到性能优化 → 只在新 JVM 生效

这时候,继续停在 Java 8 的成本开始显性化。不是系统跑不动,而是你被锁在一个越来越狭窄的选择空间里

更现实的是人员问题。新来的工程师,默认使用的已经是 Java 17 甚至更高版本。他们熟悉的是新工具链、新调试方式。当他们面对一套 Java 8 的系统时,不是学不会,而是:

很多问题的解决路径,已经不在他们的经验范围内了。

这会让“稳定”变成另一种风险。因为稳定的前提,是有人能长期维护它。到这一步,升级已经不再是“要不要”的问题。
而是变成了:

现在升级,还是被动升级?

很多团队选择继续拖延,希望把升级成本压到最低。

但现实往往是:拖得越久,升级的边界越难控制

当升级真的不可避免时,你已经不再有“慢慢试”的空间。而这,才是 Java 8 最危险的地方。它让你误以为,时间是站在你这边的。


第六章:一次相对靠谱的 JDK 升级,应该从哪里开始

真正开始升级之前,有一件事必须先想清楚:你这次升级,是为了“到达某个版本”,还是为了“验证系统能否继续演进”。

这两个目标,看起来很像,路径完全不同。很多失败的升级,问题就出在一开始选错了目标。

如果你只是想“把 Java 8 换成 17”,那你会天然倾向于:

  • 尽量不改代码
  • 尽量不动依赖
  • 尽量让系统看起来“没变”

但这种升级方式,本质上是在赌:赌 JVM 的变化不会触发你没覆盖到的路径

相对靠谱的升级,第一步反而是承认一件事:

有些问题不是升级带来的,
而是升级帮你提前暴露出来的。

所以真正的起点,往往不是生产环境,而是一个可以被随时推翻的验证环境。不是单元测试,也不是本地跑一下。而是把完整应用,用新 JDK 跑起来。不接真实流量,但一定要接真实配置、真实依赖、真实启动参数。

很多团队在这里就已经踩到了第一个坑:启动参数。Java 8 下积累了大量 JVM 参数,其中不少早已被废弃,甚至在新版本里直接失效。你会看到类似这样的警告:

Ignoring option PermSize; support was removed in 8.0

在 Java 8 你还能“假装没看到”,在新 JDK 下,它会直接提醒你:这些参数已经没有意义了。清理这些参数,本身就是一次风险排查。不是“能不能启动”,
而是启动之后,哪些地方开始行为变化。这里有一个非常实际的做法:在同一套代码下,同时跑两个版本的 JVM。

  • 一套用 Java 8
  • 一套用目标 JDK

对外提供同样的接口,跑同样的请求。不需要全量对比结果,但要盯几个关键指标:

  • P99 延迟
  • GC 行为
  • 异常日志类型是否变化

很多问题,不是“新版本一定有 bug”,而是你第一次看到了原来就存在的极端情况。还有一个经常被忽略的点:日志和监控工具本身是否适配新 JDK。有些 agent 在 Java 8 下工作得很好,但在模块化之后,注入行为发生变化。结果不是监控失效,而是监控数据“看起来正常,其实已经不完整”。

如果你在升级过程中,突然发现某些指标消失了,那不是系统变健康了,而是你少看了一部分。这也是为什么,靠谱的升级节奏通常很慢。不是因为技术上推进不了,而是你需要时间去重新建立:

“我对这个系统行为的信心。”

到这里,升级才算真正开始。不是宣布成功,而是你终于知道:

  • 哪些问题是 JDK 带来的
  • 哪些问题是历史债务
  • 哪些地方,必须在升级过程中一起解决

而这一步,几乎不可能一蹴而就。也正因为这样,很多公司在真正启动升级后,才意识到一件事:**升级 JDK,其实是在逼自己重新理解系统。**而这件事,本身就是一次不小的工程。


第七章:如果一直不升,会发生什么?

在很多团队内部,其实都默认了一种状态:

不升级,不代表现在就有问题。

这句话在相当长的一段时间里,都是成立的。Java 8 足够稳定,线上系统运行多年,没有明显的性能瓶颈,也没有无法解决的故障。于是“暂时不升”逐渐变成了“长期不升”。真正的问题,是这种状态并不是静止的。最先发生变化的,往往不是系统本身,而是它所处的环境。云厂商开始调整基础镜像;CI/CD 环境里的默认 JDK 版本往前走;安全扫描工具对旧版本的容忍度越来越低

你会发现,原来“理所当然”的前提,一个一个消失了。接着是依赖生态。一开始只是新功能不支持 Java 8,后来变成新版本直接不再测试,再后来是明确标注:不兼容。这时候你还能苟住,靠锁版本、靠私服、靠内部维护。

但代价在慢慢累积。每一次新需求评估,都会多一个隐含条件:

这个东西,能不能在 Java 8 上跑?

这个问题一旦出现得足够频繁,系统就已经被版本反向塑形了。更危险的是,问题开始延迟出现

很多在新 JDK 下会被立刻暴露的行为问题,在 Java 8 下被默默吞掉。你看不到 warning;也感受不到约束。

直到某一天,你必须升级。那时候你面对的,已经不是一次版本迁移,而是一堆被时间放大的设计问题。而升级窗口,反而更小了。

因为这次升级,不是你主动选的。可能是:

  • 安全合规要求
  • 外部依赖强制
  • 云平台策略调整

你已经没有“慢慢试”的空间。于是很多团队会在这个阶段做出一个看似合理的选择:

那就继续顶着吧,能跑一天是一天。

问题在于,这条路并不是线性的。系统越老,理解成本越高,可控范围越小。

最终你会发现,你并不是在“稳定运行一个老系统”,而是在维护一个越来越没人敢动的黑盒

这时候,Java 8 不再是你的缓冲垫,而是你的时间锁。

而你已经很难判断:

现在不升级,到底是在规避风险,
还是在把风险推给未来一个更糟糕的时刻?

这一点,很多团队只有在真正被逼到墙角时,才会意识到。


结语:也许问题不只在我们

写到这里,再回头看“为什么还卡在 Java 8”,很多原因已经很清楚了:

  • 生态复杂
  • 历史债重
  • 升级风险真实存在

但如果只停在这里,其实有点不公平。因为有一个问题,很少被正面拿出来讨论:

Java 真的做到“向下兼容”了吗?

从语法层面看,是的。Java 8 写的代码,放到 JDK 25,大多数还能编译。但从工程和运行时层面看,答案并没有这么确定。JDK 9 之后,JVM 的内部结构、边界、约束,被系统性地重构过。模块化不是补丁,是一次方向性的调整。这个调整本身没有错。甚至可以说,是 Java 走向长期可维护性的必经之路。

问题在于:这次演进的成本,几乎全部落在了使用者身上。

旧代码还能跑,但开始被警告;旧用法还能用,但需要加参数;旧依赖还能凑合,但不再被官方支持

从结果上看,JDK 并没有为“平滑迁移”提供一条真正低成本的路径。它选择的是:

保证不立刻崩,
但也不保证你能轻松往前走。

这是一种非常 Java 的工程取舍。向后兼容,被理解成“不破坏既有运行”;而不是“帮助你完成迁移”。

于是一个微妙的局面就出现了:

  • JDK 在持续演进
  • 企业系统被留在原地
  • 升级的代价,被默认为“业务方应该承担的成本”

当升级困难时,我们习惯反思自己的架构、代码、历史债。

但很少有人问一句:

如果一个平台的演进,让大多数成熟用户都不敢升级,
那这个演进路径,是否真的对“工程用户”友好?

也许这并没有标准答案。Java 选择了稳定、选择了克制、选择了长期演进。而代价,是把升级这件事,变成了一次高认知门槛的工程决策

所以今天还停在 Java 8 的团队,未必是保守,也未必是技术债失控。有时候,只是因为他们不想为一次并不完全由自己造成的不连续演进
付出过高的试错成本。

当然,这并不意味着一直停留就是对的。

只是到了 JDK 25 这个节点,也许我们该承认一件事:

Java 的升级之所以难,
并不只是因为系统老,
也因为这条升级路,本身就不够平坦。

而要不要踏上这条路,现在,依然没有一个放之四海而皆准的答案。

1

评论区

欢迎访问shiker.tech

请允许在我们的网站上展示广告

您似乎使用了广告拦截器,请关闭广告拦截器。我们的网站依靠广告获取资金。

订阅shiker.tech

文章发布订阅~

通过邮箱订阅文章更新,您将在文章发布时收到及时的邮件提醒~