昨天刷到 JDK 27 GA 的消息(build 35 转正,Mark Reinhold 在邮件列表发的公告),看了眼 JEP 列表:9 个 JEP,4 个 preview 加 1 个 incubator,没什么能上头条的大特性。搁往年这种版本我可能扫一眼就过了,但这回翻 release notes 的时候发现不对劲------这版的默认值动得有点多,对象头、GC、JFR 全都在悄悄换行为。 今天把 Linux x64 的 GA 包(jdk.java.net/27,226MB)拉下来跑了一遍,java --version 出来是这样:
java
openjdk 27 2026-09-15
OpenJDK Runtime Environment (build 27+35-2325)
OpenJDK 64-Bit Server VM (build 27+35-2325, mixed mode, sharing)
下面这些结果都是真跑出来的,不掺水。
对象头默认瘦了 8 个字节
这版我最关心的是 JEP 534(Compact Object Headers by Default)。这个特性 JDK 25 就进来了,但得手动加 -XX:+UseCompactObjectHeaders 才生效,升到 27 默认就开。 原理一句话:64 位平台上对象头从 96 位压到 64 位,12 字节的头部变 8 字节。听着像抠门优化,但 Java 应用里对象多啊,官方 release notes 说整体堆占用大概能降 20%。 官方数字归官方,我自己写了个最简单的例子实测:new 两百万个 Object 存进数组,固定 768MB 堆,各跑三遍取稳定值:
ini
默认(compact headers 开):usedHeap=24MB
-XX:-UseCompactObjectHeaders:usedHeap=40MB
200 万个对象差 16MB,算下来每个对象省 8 字节,正好是头部从 12 字节加 4 字节 padding 变成 8 字节的账。当然我这例子是极端情况,全是小对象,真实业务里混着大对象会稀释这个比例,官方说的 20% 更接近常态。 顺带说个测的时候翻的车:第一版代码把数组写成局部变量,结果 JIT 逃逸分析直接把对象分配消掉了,两组数字都是 3MB,白测一轮。想复现的记得把 holder 放到 static 字段去。 有个连带影响要单独说:UseCompressedClassPointers 被废弃了,拿旧脚本试了一下:
ini
OpenJDK 64-Bit Server VM warning: Ignoring option UseCompressedClassPointers; support was removed in 27.0
注意措辞是 Ignoring,JVM 能起来,但配置管理平台要是还在统一下发这个参数,每次启动都刷一条警告,看着心烦。建议升级前先清一遍参数清单。
G1 把 Serial 的活也接了
JEP 523 讲的是"让 G1 在所有环境当默认 GC"。以前 JVM 在资源受限的环境(比如限制了几百 MB 内存的小容器)会自动退回 Serial GC,现在不会了,只要没显式指定就一律 G1。用 PrintFlagsFinal 看默认值:
ini
bool UseG1GC = true {product} {ergonomic}
bool UseSerialGC = false {product} {default}
bool UseParallelGC = false {product} {default}
bool UseZGC = false {product} {default}
我手里有几个跑在 1C512M 小容器里的工具类服务,按以前的选择逻辑默认就是 Serial,升级后会被切到 G1。G1 自己要维护记忆集,还得跑并发线程,内存开销比 Serial 高一截,在特别小的容器里这部分占比不小。官方的说法是 G1 这几年打磨得够好,即便资源受限也不该比 Serial 差多少------"should not degrade significantly",原文就这么写的。 不过我没做正经压测,不敢替官方打包票。要是有服务跑在极限小内存容器里,升级前最好拿真实流量回放一遍,盯一下 GC 开销和 RSS。这种环境差异,官方测得再全也不如你自己的一组数据实在。
有个老参数会让 JVM 直接起不来
升级 27 最容易炸的就是这类旧参数。release notes 里列了一串被移除的选项,我挑了两个最容易踩的实测。 先试 -Xverify:none。老项目关字节码校验提速启动的祖传偏方,我在不少老代码仓库的启动脚本里都见过它。实测结果:
vbnet
$ java -Xverify:none -version
Unrecognized verification option: -Xverify:none
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
直接拒绝启动,连 -version 都跑不了。谁要是把它写在 Dockerfile 的 ENTRYPOINT 或者 K8s 的启动命令里,升级完容器直接 CrashLoop,而且报错信息跟"参数过期"对不上号,得反应一会儿才想起来是它。 相对温和的是 IHOP 改名:-XX:InitiatingHeapOccupancyPercent 改成 -XX:G1IHOP,旧参数现在只是警告:
arduino
OpenJDK 64-Bit Server VM warning: Option InitiatingHeapOccupancyPercent was deprecated
in version 27.0 and will likely be removed in a future release. Use option G1IHOP instead.
能跑,但按这个节奏,下个版本它就是下一个 -Xverify:none。手里有 JVM 调优参数的服务,这波最好一起改掉。另外 -noclassgc、-noverify、-verifyremote 也一起被移除了,都在同一份 release notes 里,升级前建议把启动参数全量 grep 一遍。
JFR 开始自动帮你抹敏感信息
JEP 536 是个容易被忽略但挺贴心的变化:JFR 录制里的命令行参数、环境变量、系统属性,遇到疑似敏感的键名会自动打码再落盘。我起了个带"密码"的系统属性做录制:
ini
$ java -Ddb.password=SuperSecret123 -Dapp.token=abc12345 \
-XX:StartFlightRecording=filename=rec.jfr JfrApp
录完用 jfr print 看这个事件:
ini
key = "db.password"
value = "[REDACTED]"
键名还在,值没了。我又在整个录制文件里 grep 明文,一个都搜不到。默认过滤规则覆盖了 password、token、secret、credential 这类常见键名,也能用 -XX:FlightRecorderOptions:redact-key 加自定义规则。以前 JFR 文件随手扔给同事排查问题是常事,这心理负担算是小了一圈。
预览特性里 Lazy Constants 值得盯一下
9 个 JEP 里有 4 个 preview、1 个 incubator,大部分还在反复打磨,跑下来我觉得 Lazy Constants(JEP 531,第三次预览)是里面最实用的。 它解决的是个老矛盾:final 字段必须构造时就赋值,想延迟初始化就得用可变字段加判空,JVM 又没法对可变字段做常量折叠。LazyConstant 把"延迟初始化"和"不可变"合并了:
rust
static final LazyConstant<String> DB_URL = LazyConstant.of(() -> {
System.out.println("[trace] loading db config from remote...");
return "jdbc:postgresql://prod.db.internal:5432/app";
});
跑出来的效果:
arduino
app started, config not loaded yet
[trace] loading db config from remote...
connect: jdbc:postgresql://prod.db.internal:5432/app
connect again: jdbc:postgresql://prod.db.internal:5432/app
第一次 get() 才加载,第二次直接用缓存值,trace 只打了一次。官方文档明确说多线程下也保证只算一次,JVM 还能把它当真常量做优化。Spring 的 @Lazy 用了这么多年,现在 JDK 层面给了个标准件。当然它还是 preview,编译运行都得加 --enable-preview,编译器也会提醒一句 "uses preview features of Java SE 27",生产别急着上,测试环境先玩玩。
另外几个快速带过:结构化并发(JEP 533,第七次预览)我用 StructuredTaskScope.open() 开 scope、fork 两个子任务再 join,跑通了基本流程;原始类型模式(JEP 532)让 v instanceof int 和 switch 里的 case int 成为可能,实测能编译能跑;Vector API 第 12 次进 incubator,从 JDK 16 算起五年没转正,这个耐心我是真服气;后量子 TLS 1.3 混合密钥交换(JEP 527)和 PEM 编码(JEP 538)跟多数业务开发关系不大,知道有这回事就行。
升级前就干三件事
生产项目求稳,25 LTS 还是首选,27 是非 LTS 版本。但你要是本来就打算跟非 LTS,27 值得升------省内存是白捡的,G1 全面接管对多数服务无感。真正要动手的就两件事:把启动脚本里的 -Xverify:none 清了,参数清单里被废弃的项顺手改掉。要是还有服务跑在极限小容器里,升级完多观察一眼 GC,别全信"should not degrade significantly"。
写完这篇顺手翻了眼手头几个老项目的启动脚本,心里有点虚------JVM 参数都有几年没动过了,回头得挨个 grep 一遍再升级。