深入解析:JDK 17 LTS 版本介绍及关键新特性概览
Java,作为全球最受欢迎的编程语言之一,其发展与演进从未停歇。Oracle 及其背后的开源社区 OpenJDK 遵循着每六个月发布一个新版本的节奏,持续为平台注入活力。然而,对于企业和长期稳定的应用而言,他们往往更青睐那些提供长期支持(Long-Term Support, LTS)的版本。在继 JDK 8 和 JDK 11 之后,JDK 17 于 2021 年 9 月正式发布,成为了 Java 生态中的又一个重要里程碑式的 LTS 版本。
本文将深入探讨 JDK 17 的重要性,详细介绍其带来的关键新特性,并概述从旧版本(尤其是 JDK 8 和 JDK 11)迁移到 JDK 17 时需要考虑的事项。
一、 JDK 17:新的 LTS 里程碑
在理解 JDK 17 的意义之前,我们首先需要了解 Java 的发布周期和 LTS 概念。自 JDK 9 开始,Java 采用了严格的六个月发布周期。这意味着每年的 3 月和 9 月都会有一个新的 JDK 版本诞生。这种快速迭代使得 Java 平台能够更快地引入新特性、改进性能、增强安全性。
然而,对于需要长时间维护和支持的生产环境应用来说,每六个月升级一次 JDK 版本是不现实的。LTS 版本应运而生,它们通常每三年发布一次,并提供多年的支持(通常由 Oracle 提供 3 年的免费更新,以及更长时间的商业支持)。JDK 8 于 2014 年发布,是第一个现代意义上的 LTS 版本(尽管它发布时还没有“LTS”这个正式标签,但其支持周期和影响力使其被广泛视为 LTS)。JDK 11 于 2018 年发布,是第二个。而 JDK 17,作为第三个重要的 LTS 版本,承载着前两个 LTS 版本之间长达六年(相对于 JDK 11 则是三年)的累积改进和创新。
选择 LTS 版本意味着你可以获得:
- 长期稳定性与支持: LTS 版本会持续接收安全补丁、Bug 修复和性能优化更新,无需频繁升级到非 LTS 版本。
- 更广泛的生态系统支持: 各种框架、库、工具和中间件会优先且更全面地支持 LTS 版本。
- 降低升级成本: 相对于非 LTS 版本,LTS 版本之间的升级间隔更长,使得企业有更充足的时间进行规划、测试和迁移。
JDK 17 的发布,为仍在 JDK 8 或 JDK 11 徘徊的企业提供了一个极具吸引力的现代化目标版本。从 JDK 8 迁移到 JDK 17,你将跨越整整 9 个非 LTS 版本(JDK 9 到 JDK 16),获得海量的新特性和改进。从 JDK 11 迁移,虽然跨度较小,但仍然能享受到 6 个版本(JDK 12 到 JDK 16)带来的红利。
二、 从 JDK 8 / JDK 11 到 JDK 17 的累积改进
要全面理解 JDK 17 的价值,不能仅看其自身版本引入的特性,更要看它自上一个 LTS 版本(JDK 11)乃至更早的 LTS 版本(JDK 8)以来累积的所有变化。这是一个巨大的飞跃。
自 JDK 8 以来的显著累积改进(JDK 9 – JDK 16):
- 模块化系统 (Jigsaw, JDK 9): 彻底改变了 Java 的组织方式,提高了安全性、可靠性和性能。
- JShell (JDK 9): 交互式 Java REPL (Read-Eval-Print Loop),极大地提高了学习和实验 Java 代码的效率。
- 类型接口推断 (var, JDK 10): 局部变量类型推断,简化了代码编写。
- ZGC 和 Shenandoah (JDK 11+): 低延迟垃圾回收器,解决了传统 GC 在大堆内存下的暂停时间问题。
- Text Blocks (JDK 15): 文本块,简化了多行字符串的创建,特别是包含 HTML、JSON、SQL 等文本。
- Records (JDK 14/15/16): 记录类,简化了不可变数据载体的创建,减少了大量的样板代码。
- 密封类 (Sealed Classes, JDK 15/16): 限制类的继承,增强了类型系统的控制力。
- switch 表达式增强 (JDK 12/13/14): switch 可以作为表达式使用,并引入了
->
箭头语法和yield
关键字。 - NullPointerExceptions 改进 (JDK 14): 提供了更详细的 NPE 信息,帮助快速定位问题。
- Vector API (Incubator): 用于加速向量计算。
- Foreign-Memory Access API (Incubator): 安全高效地访问堆外内存。
- Socket API 重新实现 (JDK 15/16): 使用 Project Loom 的基础设施改进网络编程。
- 各种性能优化: JVM 启动速度、运行时性能、内存占用等方面的持续改进。
- 安全增强: TLS 1.3 支持、加密算法更新、证书处理改进等。
- 工具链改进: jlink, jdeps, jcmd 等工具的功能增强。
自 JDK 11 以来的显著累积改进 (JDK 12 – JDK 16):
在 JDK 11 已经包含了模块化、var
、ZGC/Shenandoah 的初始版本等特性。从 JDK 11 到 JDK 17,主要的亮点包括:
- Text Blocks (JDK 15 Standard)
- Records (JDK 16 Standard)
- Pattern Matching for
instanceof
(JDK 16 Standard) - Sealed Classes (JDK 17 Standard)
- 更成熟的 ZGC 和 Shenandoah GC
- 更多的孵化和预览特性(如 Vector API, FFM API)
- 以及 JDK 17 自身引入的关键特性。
因此,升级到 JDK 17 不仅仅是为了使用 JDK 17 独有的新特性,更是为了拥抱过去六/三年来 Java 平台的整体进步。
三、 JDK 17 关键新特性概览
JDK 17 本身引入了 14 个 JEP (JDK Enhancement Proposal,JDK 增强提案)。我们将重点关注其中对开发者影响较大或具有里程碑意义的特性。
3.1 JEP 391: macOS AArch64 Port (macOS 上的 AArch64 移植)
这是一个硬件相关的特性,但对使用 Apple Silicon (M1, M2, M3 等) 芯片的 Mac 用户至关重要。此前,在这些新 Mac 上运行 JDK 需要使用 x64 仿真模式(Rosetta 2),这会带来一定的性能开销。
JEP 391 提供了对 macOS AArch64 架构的原生支持。 这意味着 JDK 可以在 Apple Silicon 芯片上以原生速度运行,带来更好的性能和能效。对于开发者来说,这意味着在 M 系列芯片的 Mac 上编译、运行 Java 应用将更加流畅和高效。
- 影响: 主要惠及 macOS 用户,提升开发体验和应用性能。
3.2 JEP 398: Deprecate the Applet API for Removal (废弃 Applet API 以便移除)
Applet 技术允许在浏览器中运行 Java 小程序,但在现代 Web 开发中早已被 JavaScript、HTML5 等技术取代,且存在安全风险和维护困难。
JEP 398 正式将 Applet API 标记为废弃 (deprecated) 并明确指出它将在未来的版本中被移除。 这标志着 Java 对这一过时技术的正式告别。
- 影响: 对于仍在使用 Applet 技术的遗留系统,需要尽快迁移。对于大多数现代 Java 应用,此项改动没有影响。
3.3 JEP 403: Strongly Encapsulate JDK Internals (强封装 JDK 内部 API)
这是 JDK 17 中最重要(也可能是最具“破坏性”)的特性之一,它是自 JDK 9 引入模块化以来 Jigsaw 项目的又一步。
JEP 403 默认禁止从外部访问 JDK 的内部 API。 在 JDK 9 到 JDK 16 中,虽然模块化系统已经存在,但为了兼容性,默认设置允许通过反射等方式访问大多数 JDK 内部 API(例如 sun.misc.Unsafe
, sun.reflect
, com.sun
包下的类等),只是会发出警告。而 JDK 17 将默认行为更改为 --illegal-access=deny
。这意味着如果你的代码或依赖库尝试非法访问 JDK 内部 API,将直接抛出 IllegalAccessException
。
- 为什么重要: JDK 的内部 API 是非公开的、不稳定的,它们随时可能在未来版本中改变或移除。依赖内部 API 会导致应用与特定 JDK 版本紧密耦合,难以升级,并可能存在安全隐患。强封装使得 JDK 内部实现可以更自由地演进,提高了平台的稳定性和可维护性。
- 影响: 这是从 JDK 8 或 JDK 11 迁移到 JDK 17 时最常见的兼容性问题来源。许多老的库或框架可能依赖了 JDK 内部 API。你需要升级这些依赖到支持 JDK 17 的版本,或者在无法升级的情况下,使用命令行参数
añadiendo --add-opens <module>/<package>=<target-module>
来放宽特定模块特定包的访问限制(但这只应作为临时解决方案)。
3.4 JEP 409: Sealed Classes (密封类) – 正式特性
密封类在 JDK 15 和 16 中作为预览特性引入,并在 JDK 17 中正式转正。
密封类允许你明确声明一个类或接口可以被哪些特定的类继承或实现。 这为类的继承结构提供了更强的控制力。
-
语法: 使用
sealed
关键字修饰类,并使用permits
关键字列出允许继承/实现的类。子类必须声明为final
,sealed
, 或non-sealed
。
“`java
public sealed class Shape permits Circle, Rectangle, Square {
// …
}public final class Circle extends Shape { … } // 允许的 final 子类
public sealed class Rectangle extends Shape permits FilledRectangle { … } // 允许的 sealed 子类
public non-sealed class Square extends Shape { … } // 允许的 non-sealed 子类 (可以被任意类继承)
``
default` 分支,提高了代码的健壮性和可读性。
* **为什么重要:**
* **增强类型系统:** 使得编译器和开发者都能确切知道一个密封类的所有可能的直接子类。
* **改进模式匹配:** 与未来的模式匹配特性结合(特别是 switch 的模式匹配,尽管 JEP 406 在 17 中是预览),编译器可以判断 switch 块是否已经覆盖了所有可能的子类,从而避免需要
* 提高安全性: 防止意外的或恶意的子类化。
* 影响: 这是一个非常有用的语言特性,有助于构建更安全、更易于分析和维护的类层次结构。在需要封闭类层次结构时,应该优先考虑使用密封类。
3.5 JEP 406: Pattern Matching for switch (用于 switch 的模式匹配) – 预览特性
这是一个重要的语言特性,旨在简化复杂的条件逻辑判断,并在 JDK 17 中作为第一个预览版本引入(后续在 JDK 18, 19 中继续预览,最终在 JDK 21 中转正)。
JEP 406 扩展了 switch
表达式和语句的功能,允许在 case
标签中使用模式(而不仅仅是常量)。 最直接的应用是类型模式匹配,可以结合类型测试和变量绑定。
- 语法示例 (JDK 17 预览):
java
Object o = ...;
String formatted = switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
}; - 为什么重要:
- 简化代码: 大幅简化了使用
if-instanceof
链进行类型判断和转换的常见模式。上面的 switch 表达式替代了多层if (o instanceof Type) { Type t = (Type) o; ... }
的冗长写法。 - 提高可读性: 意图更清晰。
- 结合密封类: 当与密封类一起使用时,编译器可以进行穷举性检查(是否覆盖了所有允许的子类),从而可能无需
default
分支,减少错误。
- 简化代码: 大幅简化了使用
- 影响: 作为预览特性,需要启用相应的编译和运行参数 (
--enable-preview
)。开发者可以开始尝试使用,并提供反馈。在生产环境广泛使用前,通常建议等待其转正。
3.6 JEP 412: Foreign Function & Memory API (外部函数和内存 API) – 孵化特性
这是一个旨在彻底取代 JNI (Java Native Interface) 的重要项目,在 JDK 16 中首次孵化,并在 JDK 17 中继续孵化。
FFM API 提供了一种更安全、更高效、更易用的方式,让 Java 程序能够调用外部函数(例如 C/C++ 库中的函数)以及安全地访问外部内存(如堆外内存)。
- 为什么重要:
- 替代 JNI: JNI 使用复杂、容易出错(特别是内存管理),且存在安全风险。FFM API 旨在提供一个更好的替代方案。
- 高性能互操作: 允许 Java 代码高效地利用现有的原生库。
- 安全: 提供内存访问的边界检查,减少了 JNI 中常见的段错误风险。
- 灵活: 支持多种外部函数调用约定和内存布局。
- 影响: 作为孵化特性,API 可能会在未来版本中发生变化。主要面向需要与原生代码深度集成的场景。对于大多数应用开发者,直接影响不大,但它为底层库和框架的实现提供了更强大的能力。
3.7 JEP 414: Vector API (矢量 API) – 第二次孵化特性
Vector API 在 JDK 16 中首次孵化,并在 JDK 17 中继续孵化,并进行了一些改进。
Vector API 旨在允许开发者在 Java 代码中表达向量计算,并能够在运行时将其编译为底层 CPU 架构上的优化向量指令(如 x86 的 SSE/AVX、ARM 的 NEON)。 向量计算是指一次性对多个数据元素执行同一操作,这在科学计算、机器学习、图像处理等领域非常重要。
- 为什么重要:
- 高性能: 通过利用 CPU 的向量单元,可以在数据并行任务中实现显著的性能提升,远超传统的标量操作。
- 跨平台: API 设计旨在能够适配不同的 CPU 架构,由 JVM 负责将其映射到最优的向量指令集,而不是依赖特定的 JNI 代码。
- 易用性: 相比直接编写汇编或使用原生的向量内在函数,Java 的 Vector API 提供了更高级别的抽象。
- 影响: 作为孵化特性,API 可能会发生变化。主要面向对性能要求极高且涉及大量数据并行计算的特定领域应用。
3.8 JEP 415: Context-Specific Deserialization Filters (上下文特定的反序列化过滤器)
这是一个重要的安全特性。Java 的对象反序列化是历史上发现过多次严重安全漏洞的领域(例如著名的 Apache Commons Collections 反序列化漏洞)。恶意攻击者可以通过精心构造的序列化数据,利用应用程序 classpath 中存在的 Gadget 类,在反序列化过程中执行任意代码。
JEP 415 允许在反序列化流处理期间,基于上下文动态地配置和应用反序列化过滤器。 这意味着应用程序可以在不同的场景下(例如,处理来自不同来源的数据),应用更细粒度的过滤策略,只允许反序列化预期的类集合。
- 为什么重要: 显著增强了 Java 反序列化的安全性,提供了更灵活的防御机制来抵御反序列化相关的攻击。
- 影响: 对于所有涉及反序列化操作的应用(特别是接收来自不可信源的序列化数据的应用),都应该关注并考虑利用此特性来增强安全性。
3.9 JEP 306: Restore Always-Strict Floating-Point Semantics (恢复严格的浮点语义)
在 JDK 1.2 中,为了提升性能,Java 放宽了对浮点运算的一些严格性要求(特别是关于扩展指数)。这意味着在某些情况下,默认的浮点计算结果可能与 IEEE 754 标准不完全一致,尤其是在不同的硬件平台上。strictfp
关键字可以用于恢复严格模式,但使用起来不够方便。
JEP 306 废除了在特定情况下(如默认方法、lambda 表达式中的方法)对浮点严格性的放宽,恢复了全局的严格浮点语义(除非明确使用非严格模式)。 这使得浮点计算的结果更加可预测和跨平台一致。
- 影响: 对于大多数应用,影响很小或没有影响。对于那些对浮点计算精度和可移植性有严格要求的应用(如科学计算、金融计算),这是个积极变化,保证了结果的一致性。极少数依赖于旧的非严格行为以获得微小性能提升的代码可能会受到轻微影响。
3.10 其他 notable JEPs in JDK 17:
- JEP 410: Remove the Experimental AOT and JIT Compiler (移除实验性的 AOT 和 JIT 编译器): 移除了实验性的 GraalVM JIT 编译器(作为替代可以通过 JVMCI 接口使用外部编译器)以及实验性的 AOT (Ahead-Of-Time) 编译器
jaotc
。这些实验性特性并没有获得广泛应用和支持,因此被移除以简化 JDK。 - JEP 411: Deprecate the Security Manager for Removal (废弃安全管理器以备移除): Security Manager 是一个非常老的安全机制,在现代 Java 应用中很少使用,且配置复杂。此 JEP 将其标记为废弃,并计划在未来版本中移除。
- JEP 407: Remove RMI Activation (移除 RMI 激活机制): RMI Activation 机制是 RMI 的一个可选部分,同样很少使用且存在安全和实现复杂性问题,因此被移除。
- JEP 416: Reimplement Core Reflection with Method Handles (使用 Method Handles 重写核心反射): 这是一个内部实现细节,使用 Method Handles API 重写了
java.lang.reflect
的核心部分,旨在提高反射的性能和可维护性。 - JEP 418: Internet-Address Resolution SPI (互联网地址解析 SPI): 定义了一个服务提供者接口 (SPI),允许替换 JDK 默认的域名解析器实现(基于操作系统)。这对于需要自定义 DNS 解析行为的场景(如服务发现、特殊网络环境)非常有用。
四、 从旧版本迁移到 JDK 17 的注意事项
迁移到新的 LTS 版本通常涉及一些工作,特别是从 JDK 8 或 JDK 11 这样跨度较大的版本迁移。
-
强封装 (JEP 403): 这是最可能导致问题的变化。
- 测试: 在 JDK 17 上运行现有应用和测试套件,观察是否有
IllegalAccessException
或警告信息。 - 依赖升级: 更新所有第三方库和框架到支持 JDK 17 的最新版本。这是解决内部 API 访问问题的首选方法。
- 使用
--add-opens
(临时方案): 如果无法升级依赖,可以使用--add-opens
命令行参数来临时允许对特定内部包的访问。例如,如果你的应用依赖了sun.misc.Unsafe
,可能需要添加--add-opens java.base/sun.misc=ALL-UNNAMED
。务必谨慎使用,并将其视为临时措施,最终目标是消除对内部 API 的依赖。 - 扫描工具: 使用如 JDeprScan 或 OpenJDK 的 jdeps 工具来检查代码和依赖中是否存在对废弃或内部 API 的使用。
- 测试: 在 JDK 17 上运行现有应用和测试套件,观察是否有
-
GC 变化: 默认 GC 可能已从 Parallel GC 变为 G1 GC (自 JDK 9)。虽然 G1 通常性能更好,但对于某些特定工作负载,可能需要进行一些 GC 调优。如果之前使用了实验性的 GC,如 ZGC 或 Shenandoah,它们在 JDK 17 中已经更加成熟,但配置选项可能有所变化。
-
移除的特性: 确认你的应用没有使用 JDK 17 中已移除的特性,如 Applet API (虽是废弃但移除指日可待), RMI Activation, 实验性 AOT/JIT 编译器等。
-
模块化 (JDK 9+): 如果你从 JDK 8 迁移,需要理解模块化系统的影响。虽然大多数遗留应用可以在 classpath 上运行而无需模块化,但理解模块边界有助于解决类加载问题以及与 JEP 403 相关的强封装问题。
-
新的关键字和 API: JDK 9-16 引入了许多新的关键字 (
var
,records
,sealed
,permits
,yield
等)。如果你的代码中恰好使用了这些词作为标识符,需要进行重命名。新的 API 和库的变化也需要关注。 -
Tooling 和 IDE 支持: 确保你的构建工具 (Maven, Gradle)、IDE (IntelliJ IDEA, Eclipse, VS Code) 和 CI/CD 环境支持 JDK 17。
-
全面测试: 在迁移后进行彻底的功能测试、性能测试和压力测试,确保应用在 JDK 17 上稳定、正确运行。
五、 总结与展望
JDK 17 是一个具有里程碑意义的 LTS 版本,它不仅仅是简单的新特性集合,更是 Java 平台自 JDK 11 以来在模块化、语言特性、虚拟机性能、安全性等方面的集大成者。
从 JDK 8 或 JDK 11 升级到 JDK 17,你将获得:
- 显著的性能提升: 来自于 JVM 的持续优化、更先进的 GC 算法以及特定硬件(如 Apple Silicon)的原生支持。
- 更高的开发者生产力: 记录类、文本块、switch 表达式增强、
var
等语言特性让代码更简洁、更易读。 - 增强的安全性: 强封装、反序列化过滤器、废弃不安全特性等提升了平台的整体安全水平。
- 更好的可维护性: 模块化、密封类等特性帮助构建更结构化、更易于理解和维护的应用。
- 对未来特性的支持: Vector API, FFM API 等孵化/预览特性为 Java 的未来发展方向奠定了基础。
虽然从旧版本迁移到 JDK 17 可能需要一些努力,特别是应对强封装带来的兼容性挑战,但考虑到 LTS 版本带来的长期支持、稳定性和巨大的技术进步,这绝对是一项值得投资的工作。JDK 17 的发布,标志着 Java 平台进入了一个新的稳定发展周期,为开发者和企业提供了拥抱现代 Java 生态的坚实基础。
现在是时候开始规划你的 JDK 17 升级之路了!