去年九月JDK 25发布,作为新的LTS版本,我当时没急着升。JDK 21我们用得挺好,虚拟线程也开了,没什么非升不可的理由。
真正让我动念头的是今年年初的一次故障排查。我们一个核心服务在高并发下出现了诡异的内存占用,排查到最后发现是JDK 21的一个已知问题——具体细节不说了,反正是LTS版本也会有的老毛病。那会儿我开始认真考虑升级。
从立项到灰度完成,前后折腾了一个多月。中间踩的坑比预想的多,而且很多坑在官方文档里根本找不到,全靠社区讨论和试错。写下来,给准备升级的兄弟省点时间。
先说结论
JDK 25值得升,尤其是你还停留在JDK 17或更老版本的话。虚拟线程已经成熟了,结构化并发也有预览版,String Templates转正了,对象头从96位压缩到64位,内存占用平均能降10%-20%。
但升级不是换个JDK版本那么简单。我们升级过程中遇到的实际问题,我按踩坑顺序写。
坑1:虚拟线程的默认线程数,别照抄网上的配置
JDK 21里虚拟线程刚出,网上教程千篇一律教你怎么开:
spring:
threads:
virtual:
enabled: true
开了之后确实爽,Tomcat直接跑虚拟线程上,连接数上限一下从200变成几千。但JDK 25里虚拟线程已经是成熟特性,很多组件的行为和21不一样了。
我们踩的坑是:JDK 25里Tomcat对虚拟线程的默认处理变了,连接数和超时配置的语义和21完全不同。照着21的经验配,生产环境上线当天连接就爆了,用户请求大量超时。后来查官方文档才发现,JDK 25的Tomcat版本里,server.tomcat.threads.max在虚拟线程模式下默认值都变了,而且虚拟线程下这个配置基本没意义,应该调整的是server.tomcat.accept-count和连接数。
一句话总结:网上那些JDK 21的虚拟线程教程,升级后别照搬。
坑2:第三方库的兼容性,比你想的严重
升级前我列了个清单,把项目里的依赖全过了一遍,重点看字节码操作类库(CGLIB、ByteBuddy、ASM)、动态代理库、序列化库(Kryo、FST)、监控agent。
结果还是漏了一个:一个老的日志库的agent模块,直接在JDK 25下启动报UnsupportedOperationException,查下来是它内部用了JDK 25移除的内部API。
经验:升级前不要只跑单元测试,一定要在生产同规格的镜像里跑一遍全量集成测试。 单元测试很多都是Mock的,根本走不到那些第三方库的真实调用路径。我们就是因为跳过了这一步,白白在生产灰度时排查了一个下午。
还有一点,别用老的构建工具。Maven 3.6以下对JDK 25的支持有问题,编译期会报奇怪的错误。升级JDK的同时,把Maven、Gradle、IDE全升到支持JDK 25的版本,这个钱和时间省不了。
坑3:String Templates,别急着大规模用
JDK 25把String Templates转正了,这玩意确实好用:
// 以前
String msg = "用户 " + user.getName() + " 下单了 " + order.getAmount() + " 元";
// 现在
String msg = STR."用户 \{user.getName()} 下单了 \{order.getAmount()} 元";
可读性强了不少,尤其拼复杂日志和SQL的时候。但我们没敢大规模用,原因很实在:
这个特性在生产环境踩坑的成本太高。 模板里如果表达式抛异常,报错信息不如以前拼接直观,而且团队里不是所有人都熟悉这个语法,混着写很容易出问题。我们的策略是:新代码可以用,存量代码不动。等团队都熟了再逐步迁移。
顺便说一句,网上有些文章写STR模板要引入包,那是错的。JDK 25里这是语言内建的,不需要任何import。
坑4:对象头压缩带来的连锁反应
JDK 25的紧凑对象头(compact object headers)是这次升级里最实在的优化,对象头从96位缩到64位,堆里对象多了能省不少内存。
但副作用是:JOL(Java Object Layout)这类内存布局分析工具、以及一些依赖对象头做特殊操作的库,会受到影响。 我们有个内存分析工具直接失效了,显示的对象大小和实际完全对不上。
还有,如果你用了-XX:+UseCompressedOops相关的参数调优,JDK 25里默认行为有变化,别再用老的调优参数,先跑起来看默认表现,再用JFR和JMC去实际观测,缺什么再补什么。
坑5:反射限制,你项目的框架可能中招
JDK 25开始为"final字段不可变"做准备,用反射去改final字段会打警告。这本来是好事,安全性和不变性都提升了。但对一些老框架和写法很不友好。
我们有个老代码,用反射往final的静态字段里注入测试值,升级后测试环境开始疯狂打警告。虽然还能跑,但这是给未来埋雷——JDK 26已经正式限制这个行为了,到时候不修直接报错。
建议升级的时候顺手把这些依赖反射改final的代码清一遍,早晚要还的债,不如现在还。
坑6:G1 GC的行为变化,监控告警规则要跟着改
JDK 25对G1做了优化,GC暂停时间更短了。但GC日志的格式变了,我们监控平台的解析规则直接失效,升级当天告警刷屏。
升级JDK的同时,一定记得把GC日志格式的解析规则、监控平台的版本一起评估。 这个很容易被忽略,但生产环境出问题的时候特别致命——你想查GC日志,结果监控平台解析不了,等于瞎了。
升级路线图,我建议这样做
如果你也在考虑升级,按这个顺序来,能少踩一半的坑:
- 先升级开发环境,让团队全员用JDK 25跑两周,有问题尽早暴露
- 编译期先过,处理掉废弃API的警告,别无视
- 全量集成测试,尤其关注第三方库、agent、序列化这些容易出问题的环节
- 灰度环境跑一周,重点观察GC、内存、线程池表现,对比升级前后
- 逐步放量,先接5%流量,再30%,再全量,每步观察24小时
我们这次全流程走下来,最深的感受是:升级JDK的收益是真的,但风险也真实存在,关键是把测试做足。 千万别觉得"只是换个版本号"。
如果这篇对你有点用,点个关注。后面我计划写JDK 26的Project Detroit——Java直接调Python AI库的实战,感兴趣的话可以蹲一下。