JDK 27 新特性速览:这 9 个变化会影响你的代码
如果你的项目还在用 JDK 17,这篇文章你得看完。JDK 27 明天(8 月 6 日)发布 RC 版本,9 月 15 日正式 GA。虽然它是一个短期支持版本(非 LTS),但这次带了 9 个 JEP,其中 G1 成为唯一默认 GC、结构化并发接近正式版、对象头从 96 位压缩到 64 位——每一个都直接关系到你线上服务的性能和内存开销。这篇文章不列清单,挑 5 个对你影响最大的特性,讲清楚它到底改了什么、对你的代码意味着什么、你现在该不该为此做点什么。
写作日期:2026 年 8 月 5 日,基于 JDK 27 EA Build 33 + OpenJDK JEP 文档。
一、G1 成为全场景默认 GC:你的小容器可能变快了
这是 JDK 27 影响面最大的一个变化。
从 JDK 9 开始,G1 已经是服务器环境(至少 2 个 CPU + 1792MB 内存)的默认 GC。但在小规格环境(单 CPU 或内存低于 1792MB)下,JVM 会自动退回到 Serial 收集器。
JDK 27 把这个限制拿掉了。无论多少 CPU、多少内存,只要你不手动指定 GC,JVM 就选 G1。
为什么现在可以这么干?
OpenJDK 团队在 JEP 523 里给出了数据支撑:经过 JDK 9 到 27 的多轮优化(包括 JEP 522 对 G1 同步机制的削减),G1 在小规格环境下的吞吐量已经接近 Serial,而延迟始终优于 Serial——因为 G1 用增量回收代替了 Serial 的全堆 Stop-The-World。
具体来说,Serial 在旧一代回收时必须做 Full GC,此时所有工作线程全停。而 G1 的 Mixed GC 只回收一部分 Region,停顿时间可控。
对你的影响
如果你在跑 Docker 容器——比如一个 512MB 内存、单核的服务——以前它用的是 Serial GC,升级到 JDK 27 后会自动切到 G1。大多数场景下这是好事,延迟会更稳定。但要注意一点:G1 的内存占用比 Serial 略高(需要记住 Region 的 Remembered Set),如果你的容器内存卡得特别紧(比如 256MB 以下),建议做一下基准测试对比。
如果你本来就手动指定了 GC(比如-XX:+UseZGC或-XX:+UseParallelGC),这个变化跟你没任何关系——显式指定的优先级永远高于默认值。
怎么验证
# 升级前看看当前用的什么 GCjava-XX:+PrintCommandLineFlags-version2>&1|grepGC# JDK 27 上不加任何 GC 参数,输出应该包含 -XX:+UseG1GC二、结构化并发第七次预览:离正式版又近一步
结构化并发(JEP 533)是从 JDK 19 就开始孵化的特性,经历了 7 个版本迭代,API 已经相当成熟。它跟虚拟线程是天生一对——虚拟线程让你创建百万级轻量线程,结构化并发让你管理这些线程的生命周期。
核心思想
传统的并发代码用ExecutorService+Future,线程的父子关系完全靠你自己用代码维护。一个任务 fork 出两个子任务,如果其中一个失败了,另一个得手动取消——漏写一行 cancel,那个线程就飘在那里,直到超时或服务重启。
结构化并发把这个问题反过来解决:任务树是结构化的,父任务的生命周期天然包含子任务。父任务结束时,所有子任务要么完成、要么被取消,不存在"孤儿线程"。
// JDK 27 的结构化并发 API(Preview)try(varscope=StructuredTaskScope.open(config->config)){Subtask<String>userTask=scope.fork(()->fetchUser(userId));Subtask<List<Order>>orderTask=scope.fork(()->fetchOrders(userId));// join() 等待所有子任务完成,任一个失败则取消其他scope.join();// 两个任务都成功了Stringuser=userTask.get();List<Order>orders=orderTask.get();returnnewDashboard(user,orders);}对比:传统写法 vs 结构化并发
传统写法——线程池 + Future,容易写出泄漏代码:
// 传统写法:子任务失败后另一个子任务可能没人取消,线程泄漏ExecutorServiceexecutor=Executors.newVirtualThreadPerTaskExecutor();try{Future<String>userFuture=executor.submit(()->fetchUser(userId));Future<List<Order>>orderFuture=executor.submit(()->fetchOrders(userId));Stringuser=userFuture.get();// 如果这里抛异常...List<Order>orders=orderFuture.get();// 这个 Future 没人管了}finally{executor.close();// 粗暴关掉,可能有正在跑的任务被中断}结构化并发——try-with-resources 帮你自动清理:
// 结构化并发:作用域结束时保证所有子任务状态确定try(varscope=StructuredTaskScope.open(config->config)){Subtask<String>userTask=scope.fork(()->fetchUser(userId));Subtask<List<Order>>orderTask=scope.fork(()->fetchOrders(userId));scope.join();// userTask 或 orderTask 任一失败,另一个自动被取消returnnewDashboard(userTask.get(),orderTask.get());}// 离开 try 块时,scope 保证没有活着的子线程关键差异:传统写法里异常和取消的关系是"你记住",结构化并发里是"JVM 保证"。
JDK 27 的变化
这次预览对 API 做了几处收尾式调整:
StructuredTaskScope现在有第三个类型参数R_X,控制join()可以抛什么异常——不再是一股脑抛ExecutionException- 新增
StructuredTaskScope.open()静态方法作为最简洁的入口 Joiner工厂方法的异常处理更灵活,可以通过Function自定义包装异常
什么信号
已经第七次预览了,API 收敛到这个程度,说明 OpenJDK 团队对设计方向很满意。结构化并发大概率在 JDK 28 或 29 转正。
如果你的项目已经在用虚拟线程(JDK 21+),现在就可以在 JDK 27 上试用结构化并发,配合--enable-preview参数。它是虚拟线程的"拼图的最后一块"——你有了轻量线程、有了结构化生命周期管理,就不需要再手写ExecutorService+CompletableFuture的组合拳了。
三、紧凑对象头:每个 Java 对象省 32 位内存
这是 JDK 27 的一个沉默但重磅的改动。说它"沉默"是因为你不需要改任何代码——JVM 自动生效。说它"重磅"是因为线上内存占用直接减少。
改了什什么
在 64 位架构上,HotSpot JVM 中每个 Java 对象的"对象头"(Object Header)占用 96 位:
- Mark Word:64 位(存 GC 信息、锁状态、哈希码等)
- Klass Pointer:32 位(指向类元数据,开启压缩指针后)
紧凑对象头(JEP 534)把 Mark Word 从 64 位压到 32 位。最终对象头变成 64 位(32 Mark Word + 32 Klass Pointer),每个对象节省 32 位 = 4 字节。
听起来不多?算一笔账:一个典型的微服务应用,堆里可能有 500 万个活跃对象。4 字节 × 500 万 = 20MB。而且这还没有算上对象的对齐填充减少带来的额外节省——对象大小变小了,对齐浪费也变少了。
技术原理
Mark Word 的 64 位里,很多位在大多数时候是冗余的。比如一个没被锁住、没被 GC 标记的对象,它的 Mark Word 里只有哈希码和 GC age 是有用的。JEP 534 把这些信息重新压缩编码到 32 位,同时保持与现有锁机制和 GC 算法的兼容性。
你不需要改代码,不需要改 JVM 参数,升级 JDK 后自动启用。想关掉可以加-XX:-UseCompactObjectHeaders,但除非遇到极特殊的兼容性问题,没理由关。
对你的影响
所有 Java 应用升级到 JDK 27 后,"免费"获得内存节省。对于内存敏感的容器部署场景,这可能是把 OOM 推迟的关键。对于大数据量的缓存服务(堆里对象特别多),效果尤其明显。
四、Lazy Constants:延迟初始化终于有官方支持了
痛点
// 这种代码你写过多少次?privatestaticfinalExpensiveObjectCACHE;static{CACHE=ExpensiveObject.loadFromDisk();// 启动慢得要死}static final字段在类加载时就初始化了。如果你的对象初始化代价高(读文件、连网络、解析大 XML),应用启动时间就被拉长。而volatile+ 双重检查锁定又容易写出 bug,而且编译器不能对volatile变量做常量折叠优化。
JDK 27 的方案
// 延迟常量:代码上声明为 final,行为上等第一次访问时才初始化privatestaticfinalLazyConstant<ExpensiveObject>CACHE=LazyConstant.of(ExpensiveObject::loadFromDisk);LazyConstant是线程安全的——多线程同时访问时保证只初始化一次。而且 JVM 把它当成真正的常量对待,可以做常量折叠、内联等优化,性能跟static final字段一致。
这次是第三次预览,API 进一步精简:
- 删掉了
isInitialized()和orElse()两个低层方法 - 新增
Set.ofLazy(...)工厂方法,现在 List、Set、Map 三种集合都有懒初始化版本了
LazyConstant vs volatile DCL
传统的双重检查锁定(DCL):
// 传统 DCL 单例:易错、编译器不优化privatestaticvolatileExpensiveObjectinstance;publicstaticExpensiveObjectget(){if(instance==null){synchronized(ExpensiveObject.class){if(instance==null){instance=newExpensiveObject();}}}returninstance;}LazyConstant 一行搞定:
// JDK 27 LazyConstant:线程安全、JVM 可做常量折叠优化privatestaticfinalLazyConstant<ExpensiveObject>CACHE=LazyConstant.of(ExpensiveObject::loadFromDisk);DCL 的问题不只是代码丑——volatile阻止了 JIT 编译器做常量折叠和内联优化。LazyConstant被 JVM 识别为真正的常量,编译期和运行期优化都能生效,性能优于 DCL。
适用场景
- 配置文件解析、字典加载等启动时不需要但运行时会用到的数据
- 单例模式的正统替代——不用手写 DCL
- 微服务启动优化——把非关键的初始化从启动路径上挪走
- 无状态工具类中大对象的懒加载(正则 Pattern、Jackson ObjectMapper 等)
五、其余 5 个 JEP 速览
不是每个 JEP 都对普通开发者有直接影响。剩下 5 个按重要性排序:
| JEP | 名称 | 一句话 | 影响范围 |
|---|---|---|---|
| 532 | 原始类型模式匹配 | switch和instanceof支持int/long/double等原始类型,第五次预览 | 日常编码,语法糖向 |
| 527 | 后量子 TLS 密钥交换 | TLS 1.3 支持抗量子计算攻击的混合密钥交换算法 | 金融、政务等安全敏感场景 |
| 536 | JFR 进程内数据脱敏 | JFR 录制时自动遮盖命令行参数、环境变量中的敏感信息 | 运维/安全 |
| 537 | Vector API | SIMD 向量计算,第 12 次孵化 | 高性能计算、ML 推理 |
| 538 | PEM 编码 API | 密钥和证书的 PEM 编解码标准 API,预览 | 加解密、证书管理 |
六、你该做什么
如果现在用的是 JDK 21 LTS:不用急着升级,JDK 27 不是 LTS。但可以在非生产环境试试结构化并发 + 虚拟线程的组合,提前适应代码风格。下一个 LTS 大概率是 JDK 29 或 30,结构化并发转正的可能性很大。
如果现在用的是 JDK 17 LTS:你已经落后了。JDK 17 的生命周期到 2027 年还有段时间,但 JDK 21 的虚拟线程、JDK 27 的紧凑对象头和 G1 默认化,这三个东西加起来对你的线上服务提升是实实在在的。建议直接跳到 JDK 21 LTS,JDK 27 作为下一个 LTS 的预演版本了解一下就行。
如果你是 Spring Boot 用户:确认你用的 Spring Boot 版本兼容目标 JDK。Spring Boot 4.1.0 官方支持 JDK 21+,跑在 JDK 27 上需要验证——一般向后兼容没问题,但建议先在 CI 里跑一遍完整测试。
一个具体的测试建议:找一台非核心服务、内存配置较低的容器(比如 1 核 512MB),升级到 JDK 27,把 G1 和紧凑对象头的组合效果跑一遍基准测试。你会发现 GC 停顿更平、内存占更小。这两个变化可能是你这轮升级里收益最确定的。