Administrator
发布于 2024-08-28 / 7101 阅读
50

JDK 23 新特性盘点与生产可用评估

季度技术评审上被问:JDK 23 要不要跟

8 月底的季度技术评审,架构师抛了个问题:JDK 22 已经用了小半年,JDK 23 再过半个月就 GA,咱们是继续滚动跟版本,还是钉在 JDK 21 LTS 上不动。

当时线上三个集群:订单跑 JDK 17,网关和用户中心跑 JDK 21,还有个新做的 AI 网关跑 JDK 22。版本一多,镜像、监控、问题排查的成本都在涨。所以这次评审要定的其实是一件事:我们到底按什么节奏跟 JDK

我的做法是不空谈,先拉 RC 构建实测。八月底 JDK 23 已经进 Rampdown Phase Two,特性集冻结,只修 bug,拿 RC 版本验证出来的结论跟 GA 差别不会大。

$ sdk install java 23.ea.36-open
$ java -version
openjdk version "23" 2024-09-17
OpenJDK Runtime Environment (build 23+36-2370)
OpenJDK 64-Bit Server VM (build 23+36-2370, mixed mode, sharing)

原始类型模式:instanceof 终于认基本类型了

JEP 455(Primitive Types in Patterns,预览)看着不起眼,但真写过代码就知道有多别扭。以前从 Object 里解一个数值出来,得先判包装类型再拆箱:

// 老写法
static String describe(Object o) {
    if (o instanceof Integer i) {
        return i > 100 ? "大整数 " + i : "整数 " + i;
    } else if (o instanceof Long l) {
        return "长整数 " + l;
    } else if (o instanceof Double d) {
        return "浮点 " + d;
    }
    return "其他";
}

问题在于它判断的是包装类型。如果上游塞进来的是个 short 装箱成的 Short,或者 JSON 反序列化给的是 BigDecimal,这段就全漏了。我在网关的参数校验里就踩过,前端传 {"age": 18},Jackson 默认给 Integer,改成 {"age": 18.0} 就变 Double,校验逻辑直接失效。

JDK 23 里可以这么写:

static String describe(Object o) {
    return switch (o) {
        case int i when i > 100 -> "大整数 " + i;
        case int i              -> "整数 " + i;
        case long l             -> "长整数 " + l;
        case double d           -> "浮点 " + d;
        case String s           -> "字符串 " + s;
        default                 -> "其他";
    };
}

关键区别:这里的 case int i 匹配的是而不是类型。Short s = (short) 18 会被 case int i 接住(安全窄化转换),case long l 也能接 int(安全 widening)。不安全的转换编译器直接拒绝,比如 case int i 不会去匹配 Long.MAX_VALUE 塞进来的 long

这个功能在 switch 的类型覆盖判定上还带来一个好处,以前写 case Integer i 时编译器要求有 default,现在因为基本类型族可以穷举,配合模式匹配能少写不少兜底分支。

编译记得加参数:

$ javac --release 23 --enable-preview PatternPrim.java
$ java --enable-preview PatternPrim

隐式声明类:脚本化 Java 的最后一块砖

JEP 463 第二轮预览。核心就两句话:可以没有 class 声明,main 可以不是 public static void main(String[] args)

// Main.java,全文就这么点
void main() {
    var urls = List.of("https://api.internal/health");
    urls.forEach(u -> System.out.println(u + " -> " + probe(u)));
}

String probe(String url) {
    return "200 OK";
}

单文件源码启动(JEP 330,JDK 11 就有)现在可以直接:

$ java --enable-preview --source 23 Main.java

不用 javac,不用写类名,不用 public static void main。它对我们的实际价值不是"少写几行",而是让 Java 能进运维脚本场景。我们之前有一批 Shell 脚本负责清理 ES 历史索引、对账 MQ 积压,写复杂了不好维护,写 Python 又得在跳板机上装环境。现在运维同学可以直接拿 Java 写,享受 HttpClient、JDBC、Stream 这些现成能力。

我在 RC 上试了个真实的:查 Redis 里某个前缀的 key 数量并导出 CSV,四十来行,java --source 23 Clean.java 一把梭。以前这活儿得开个 Maven 工程。

分代 ZGC 默认化:这次是真能开

JEP 474,ZGC 的分代模式在 JDK 23 里变成默认,非分代模式标记废弃(-XX:-ZGenerational 会打警告)。

我们的 AI 网关从 JDK 21 开始就用 ZGC,因为流式输出场景下堆积的临时对象特别多,G1 的 Young GC 频率下不来。JDK 21 里启用分代得显式加参数:

# JDK 21 时代的配置
-XX:+UseZGC -XX:+ZGenerational -Xmx8g

JDK 23 里 -XX:+UseZGC 就等于分代了。我用同一个压测脚本在两个版本上跑了一遍,场景是 1200 QPS 的流式响应,堆 8G:

指标JDK21 ZGC 分代JDK23 ZGC 分代
GC 总停顿 P991.8 ms1.1 ms
GC CPU 占用约 6.2%约 4.1%
堆水位峰值5.4 GB4.6 GB
分配停滞(Allocation Stall)次数37 次/10min4 次/10min

分配停滞次数下降是最实在的。这玩意儿一旦发生,业务线程会被卡住几十毫秒,我们网关 P99 从 210ms 抖到 700ms 就是它闹的。

不过有一点提醒:升级后如果看到日志里出现 Warning: Option -XX:-ZGenerational is deprecated,说明有人在配置里保留了关闭参数,删掉即可。

顺带看了一眼的几个

  • JEP 467 Markdown 文档注释:这个已经转正,不是预览。/// 开头可以写 Markdown,javadoc 直接渲染。我们内部工具的注释确实该迁一部分了。
  • JEP 473 Stream Gatherers(二轮预览):能自定义中间操作,比如内置的 foldmapConcurrentwindowFixedmapConcurrent 在虚拟线程下挺好用,后面有空单独写。
  • JEP 480 / 481 结构化并发与 Scoped Values:第三轮预览,还是预览。API 又变了,StructuredTaskScope 现在用 open() 静态工厂加 Joiner。这种反复改的东西坚决不进生产。
  • JEP 471 废弃 sun.misc.Unsafe 的内存访问方法:这是个信号。Netty、Disruptor 这些老牌库在 JDK 23 上启动会刷一堆 Terminal deprecation 警告,我们网关升级那会儿日志里刷了七百多行,看着吓人,功能不受影响,但说明留给这些库迁移的时间不多了。

我们的升级决策

评审会上最后定的口径,供参考:

  1. 核心交易链路继续钉 JDK 21。下一个 LTS 还得再等一年,中间这几个非 LTS 版本只有六个月支持期,升级带来的风险不值当。
  2. 新起的非核心服务可以用 JDK 23,但要接受半年后必须再升一次。AI 网关属于这类,它迭代快,晚升不如早升。
  3. 预览特性一律不开。原始类型模式和隐式声明类都属于预览,编译出来的 class 带 minor_version 65535 标记,换个 JDK 版本就跑不了。想用就等它们转正,或者只在工具脚本里用。
  4. 建立版本台账。之前最大的问题不是版本多,而是没人说得清哪个服务跑哪个版本。现在在 CI 里加了 java -version 输出埋点到镜像 label,Grafana 上能直接看分布。

非 LTS 版本不是不能上,而是要想清楚「谁为半年后的升级买单」。如果团队没有专人跟版本,钉住 LTS 是更省事的选择。

小结

这轮实测下来,JDK 23 里能立刻拿到收益的只有分代 ZGC 默认化这一项,属于"升级即白拿"。原始类型模式和隐式声明类都还是预览,前者对写通用解析代码的人很香,后者让 Java 终于能体面地写脚本,但都得再等等。

我们最后没有整体升级,只把 AI 网关切到 23 试跑一个月。真实结论等跑满一个月再写。

参考