新增特性:
1、虚拟线程(Virtual Threads)(JDK 21)
这是 JDK 21 中的一个关键特性,旨在通过提供轻量级的线程实现来改善 Java 应用程序的并发性能。虚拟线程是 Project Loom 的一部分,该项目的目标是将纤程(fibers)的概念引入 Java 平台,从而提高并发编程的易用性和效率。
虚拟线程的主要特点:
-
轻量级线程:虚拟线程比传统的操作系统线程更轻量级,它们在 Java 虚拟机(JVM)中实现,而不是由操作系统管理。
-
更高的并发性:由于虚拟线程的轻量级特性,应用程序可以创建和运行数以百万计的线程,而不会像传统线程那样受到操作系统资源限制的影响。
-
改进的响应性:虚拟线程可以帮助提高应用程序的响应性,因为它们减少了线程创建和销毁的开销。
-
简化的并发编程模型:虚拟线程使得并发编程更加容易,因为开发者可以使用他们已经熟悉的同步和并发控制机制。
如何使用虚拟线程:
在 JDK 21 中,你可以通过以下方式来使用虚拟线程:
import java.lang.Thread;
public class VirtualThreadExample {
public static void main(String[] args) {
Thread virtualThread = Thread.ofVirtual().unstarted(() -> {
// 虚拟线程执行的任务代码
System.out.println("Running in a virtual thread");
});
virtualThread.start();
}
}
在这个示例中,我们使用 Thread.ofVirtual().unstarted()
方法创建了一个虚拟线程,并提供了一个 lambda 表达式来定义线程执行的任务。然后,我们调用 start()
方法来启动虚拟线程。
注意事项:
- 虚拟线程主要作用是提升服务器端的吞吐量,而不是提高线程的执行速度。
- 虚拟线程只适合IO密集型的任务,阻塞期间可以将CPU资源让渡给其他任务,不适合CPU密集型的任务或非阻塞任务。
- 虚拟线程是廉价且轻量级的,使用完后立即被销毁,因此不需要池化。
2、 switch 表达式的模式匹配(JDK21)
这是 Java 语言中一个重要的新特性,它在 JDK 21 中被正式引入。这个特性扩展了 Java 14 中引入的 switch 表达式,允许在 switch 语句中使用模式匹配,从而提供了一种更简洁和表达力更强的方式来处理基于不同情况的逻辑。
模式匹配的主要特点:
-
类型安全:模式匹配提供了类型安全的方式来处理不同类型的数据,减少了类型转换的错误。
-
简化代码:通过使用模式匹配,可以减少样板代码,使代码更加简洁和易于阅读。
-
增强表达力:模式匹配增强了 switch 语句的表达力,允许更复杂的逻辑判断。
-
与 instanceof 的集成:模式匹配可以与 instanceof 一起使用,提供更灵活的类型检查。
如何使用模式匹配的 switch 表达式:
以下是如何使用模式匹配的 switch 表达式的示例:
public class PatternMatchingSwitch {
public static void main(String[] args) {
Object obj = "hello";
switch (obj) {
case String s -> s.length() > 5 -> System.out.println("String is long");
case String s -> System.out.println("String: " + s);
case Integer i -> System.out.println("Integer: " + i);
default -> System.out.println("Unknown type");
}
}
}
在这个示例中,我们使用了一个 switch 表达式来匹配不同的模式。对于字符串,我们进一步检查了字符串的长度,并执行了相应的操作。
3、有序集合(Sequenced Collections)(JDK 21)
它通过引入新的接口来解决现有集合类型中缺乏统一的顺序访问方法的问题。
新接口:
- SequencedCollection:提供对集合两端元素的访问方法,如
addFirst(E)
、addLast(E)
、getFirst()
、getLast()
等,并支持获取集合的逆序视图reversed()
。 - SequencedSet:扩展自
Set
和SequencedCollection
,确保集合中不包含重复元素。 - SequencedMap:扩展自
Map
,提供对映射条目的顺序访问,支持putFirst(K, V)
和putLast(K, V)
方法。
改造现有集合:
List
和 Deque
现在都直接继承自 SequencedCollection
,而 LinkedHashSet
和 LinkedHashMap
则实现了 SequencedSet
和 SequencedMap
,使得它们能够支持新的顺序操作。
优势:
通过提供统一的接口,开发人员可以更方便地处理具有定义顺序的集合,减少了代码的复杂性和不一致性。
新增的逆序视图功能使得在处理集合时可以更灵活地进行正向和反向迭代。
使用示例:
SequencedHashMap<Integer, String> map = new SequencedHashMap<>();
map.put(1, "Apple");
map.put(2, "Banana");
map.put(3, "Orange");
String fruit = map.get(2); // 输出: Banana
map.forEach((key, value) -> System.out.println(key + ": " + value));
Sequenced Collections 是 Java 集合框架的一次重大改进,使得开发人员在处理有序集合时更加高效和直观。这一特性不仅提升了代码的可读性,也增强了集合的功能。
4、ZGC分代(JDK21)
Generational ZGC 是在 JDK 21 中引入的一项新特性,它通过为年轻对象和老对象维护单独的代来扩展 Z Garbage Collector(ZGC),从而提高应用程序的性能。这一特性旨在降低分配停顿的风险,减少所需的堆内存开销,并降低垃圾收集的 CPU 开销,同时保持与非分代 ZGC 相当的吞吐量。
主要目标和优势
- 降低分配停滞的风险:通过更频繁地收集年轻对象,减少因垃圾回收导致的应用程序暂停。
- 减少堆内存开销:优化内存使用,使得应用程序可以更高效地运行。
- 降低垃圾收集 CPU 开销:通过改进垃圾回收算法,减少垃圾回收过程中的 CPU 使用。
- 保持低延迟:ZGC 设计目标是实现亚毫秒级的暂停时间,这对于对延迟敏感的应用程序尤为重要。
- 支持大堆内存:ZGC 支持从几百兆字节到数 TB 的堆大小,这对于需要处理大量数据的应用程序非常有利。
技术细节
- 堆的逻辑划分:分代 ZGC 将堆分为两个逻辑代——年轻代和老年代,每个代独立收集,使得 ZGC 可以更有效地处理年轻对象。
- 彩色指针和屏障:使用彩色指针和加载/存储屏障来跟踪对象状态和代际引用,确保应用程序在垃圾回收期间能够看到一致的对象图。
- 无多重映射内存:新设计不使用多重映射内存,简化了内存使用量的测量,并可能将最大堆大小增加到超过非分代 ZGC 的 16TB 限制。
- 优化的屏障:通过快速路径和慢速路径、记忆集屏障、SATB 标记屏障等技术优化了屏障的性能。
使用方法
在 JDK 21 中,要启用分代 ZGC,需要在 JVM 启动参数中添加 -XX:+ZGenerational
选项,如下所示:
$ java -XX:+UseZGC -XX:+ZGenerational ...
在未来的版本中,分代 ZGC 将成为默认选项,非分代 ZGC 将被逐步淘汰。
5、准备默认禁止动态代理(JDK21)
这一变更旨在重新评估服务性与完整性之间的平衡,前者涉及对运行代码的临时更改,而后者则假定运行代码不会被任意更改。需要注意的是,大多数不需要动态加载代理的工具都不会受到这一变更的影响。
在 JDK 21 中,虽然允许动态加载代理,但 JVM 会在出现这种情况时发出警告。例如,如果一个代理被动态加载,JVM 会输出以下警告信息:
WARNING: A {Java,JVM TI} agent has been loaded dynamically (file:/u/bob/agent.jar) WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information WARNING: Dynamic loading of agents will be disallowed by default in a future release
如果开发者需要工具在没有警告的情况下动态加载代理,可以在命令行上使用 -XX:+EnableDynamicAgentLoading
选项运行。这个选项允许在不发出警告的情况下动态加载代理,但在未来,默认情况下将不允许动态加载代理,除非使用这个命令行选项。
此外,如果开发者需要诊断动态加载代理的问题,可以使用 -Djdk.instrument.traceUsage
选项来获取更多信息。这个选项会导致 java.lang.instrument
API 的方法在使用时打印消息和堆栈跟踪,有助于识别错误地使用动态加载代理而不是在启动时加载代理的库。
默认禁止动态代理是为了提高 Java 应用程序的安全性,防止恶意代码利用动态加载代理的功能执行潜在的危险操作。尽管这样做可以增加应用程序的安全性,但也可能影响依赖于动态加载代理的现有代码。因此,在应用该增强提案之前需要仔细评估现有代码的依赖关系。
6、记录模式(Record Patterns)(JDK21)
记录模式的主要目标是:
- 扩展模式匹配,以解构
record
类的实例,实现更复杂的数据查询。 - 添加嵌套模式,支持更多可组合的数据查询。
记录模式的动机是基于 Java 16 中引入的类型模式(Type Patterns),它扩展了 instanceof
运算符以支持模式匹配,使得代码更加简洁且不易出错。记录模式进一步简化了数据的使用,允许开发者通过模式匹配来表达数据模型的语义意图。
以下是记录模式的一些关键点:
- 记录模式与类型模式:记录模式允许开发者直接从记录实例中提取组件,而不需要显式调用访问器方法。例如,如果有一个
Point
记录类,可以直接使用Point(int x, int y)
模式来提取x
和y
组件的值 。 - 嵌套记录模式:记录模式支持嵌套,这意味着可以匹配更复杂的对象图。例如,可以在一个记录模式中嵌套另一个模式,同时分解外部和内部记录 。
- 模式匹配的详尽性:在
switch
语句中使用记录模式时,必须处理所有可能的情况,这要求switch
语句是详尽的 。 - 类型参数推断:如果记录模式使用了原始类型(即没有指定类型参数),则类型参数会被推断出来 。
代码示例:
首先,我们定义一个简单的记录类 Point
:
public record Point(int x, int y) {}
示例 1:基本的记录模式匹配
在这个示例中,我们将使用记录模式来匹配 Point
实例,并提取其 x
和 y
组件:
public class RecordPatternExample {
public static void main(String[] args) {
Point point = new Point(10, 20);
if (point instanceof Point(x, y)) {
System.out.println("X coordinate: " + x + ", Y coordinate: " + y);
}
}
}
在这个示例中,instanceof
后面跟着的 Point(x, y)
是一个记录模式,它将 point
的 x
和 y
组件分别赋值给局部变量 x
和 y
。
示例 2:嵌套记录模式
假设我们有一个更复杂的记录类 Rectangle
,它包含两个 Point
记录作为其组件:
public record Rectangle(Point topLeft, Point bottomRight) {}
现在,我们可以使用嵌套记录模式来匹配 Rectangle
实例,并提取其 topLeft
和 bottomRight
组件:
public class RecordPatternExample {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle(new Point(0, 0), new Point(10, 20));
if (rectangle instanceof Rectangle(topLeft(topLeftX, topLeftY), bottomRight(bottomRightX, bottomRightY))) {
System.out.println("Top Left: (" + topLeftX + ", " + topLeftY + "), Bottom Right: (" + bottomRightX + ", " + bottomRightY + ")");
}
}
}
在这个示例中,我们使用了嵌套记录模式来匹配 Rectangle
实例的 topLeft
和 bottomRight
组件,并将这些组件进一步分解为它们的 x
和 y
值。
示例 3:在 switch
语句中使用记录模式
我们可以使用记录模式在 switch
语句中进行模式匹配:
public class RecordPatternExample {
public static void main(String[] args) {
Object obj = new Point(10, 20);
switch (obj) {
case Point(x, y) -> {
System.out.println("Point with coordinates: (" + x + ", " + y + ")");
break;
}
default -> {
System.out.println("Not a Point");
break;
}
}
}
}
在这个示例中,我们使用记录模式 Point(x, y)
来匹配 Point
实例,并提取其 x
和 y
组件。如果匹配成功,我们将打印出坐标信息。
7、重新实现核心反射(JDK18)
这一改变的目的是为了减少 java.lang.reflect
和 java.lang.invoke
API 的维护和开发成本。
主要变化和优势
-
降低维护成本:通过将反射机制基于方法句柄实现,可以减少维护三个不同内部机制的成本,这三个机制包括:VM本地方法、动态生成的字节码存根以及方法句柄本身。
-
提高性能:在微基准测试中,新实现的性能比旧实现快了43-57%。这是因为方法句柄允许直接调用,而旧的实现需要通过额外的Java方法来确保成员的声明类在访问前已初始化。
-
减少对
Unsafe
的依赖:在字段访问中,核心反射之前使用了sun.misc.Unsafe
API。通过这次改变,可以减少对Unsafe
的依赖,从而提高代码的可移植性和安全性。 -
支持新的语言特性:随着新语言特性的引入,如 Project Valhalla 中所设想的,修改所有三个代码路径的成本很高。通过这次改变,可以简化对新特性的支持。
实现细节
- 直接调用方法句柄:新实现对特定的反射对象执行方法句柄的直接调用。
- VM原生反射机制:仅在早期VM启动期间使用,即在方法句柄机制初始化之前。这有利于 Project Loom,因为它减少了对本地栈帧的使用。
风险和假设
- 兼容性风险:依赖于现有实现的高度特定和未记录方面的代码可能会受到影响。为了缓解这种兼容性风险,可以通过
-Djdk.reflect.useDirectMethodHandle=false
启用旧实现作为变通方法。 - 资源消耗:方法句柄调用可能比旧的核心反射实现消耗更多资源。这可能导致
StackOverflowError
或NoClassDefFoundError
。
8、UTF-8 作为 Java 默认的字符集(JDK18)
这一变化的主要目标是提高 Java 程序在依赖默认字符集时的可预测性和可移植性,同时澄清标准 Java API 在何处使用默认字符集,并在标准 Java API(控制台 I/O 除外)中全面标准化 UTF-8 。
在 JDK 17 及之前的版本中,Java 虚拟机在启动时会根据运行时环境(如操作系统、用户的区域设置等)选择默认字符集,这导致了跨平台应用中字符集不一致的问题。例如,在 Windows 上,默认字符集通常是 windows-1252
,而在 Unix 系统上则通常是 UTF-8
或其他字符集。这种不一致性会导致跨平台文本处理的复杂性和不可预测性 。
除非显式指定,否则所有文件读写、字符串处理以及字符流操作都会使用 UTF-8 编码。此外,JDK 18 允许通过命令行参数 -Dfile.encoding=UTF-8
来配置默认字符集,以兼容之前的版本 。
这一变化的优势包括:
- 增强了全球化和多语言支持,确保所有语言的文本都能正确处理和显示。
- 简化了代码,开发者无需显式指定字符集,减少了字符集相关的错误和异常。
- 向后兼容性,UTF-8 保留了 ASCII 码的向后兼容性,使得大多数现有代码无需修改即可正常运行 。
弃用特性:
1、弃用Windows 32位x86端口(JDK21)
这一决定基于多方面的考虑:
-
过时的硬件和操作系统:随着现代计算机普遍采用64位架构,32位硬件和操作系统已经逐渐被淘汰。大多数新版本的 Windows 操作系统也只提供64位版本。
-
性能限制:32位架构限制了可寻址内存空间的大小,无法充分利用现代计算机的资源。64位架构可以提供更大的内存地址空间,从而提高应用程序的性能和扩展性。
-
安全问题:32位架构存在一些安全漏洞和限制,例如缓冲区溢出等。64位架构通过引入更多的保护机制来增加应用程序的安全性。
-
技术限制:在 Windows 32位 x86 上,由于技术限制,某些新功能(如虚拟线程)必须回退到内核线程,无法实现预期的效益。
-
操作系统生命周期:Windows 10 是最后一个支持32位操作的 Windows 操作系统,将于2025年10月结束生命周期。
对于开发者和用户来说,这意味着在未来的版本中,将无法在32位 Windows 系统上构建或运行 Java 应用程序。尽管如此,对于仍然需要支持遗留系统的用户,可以通过添加 --enable-deprecated-ports=yes
配置选项来继续构建和使用32位 JDK,但会收到弃用警告。
2、弃用Finalization(JDK21)
Finalization 是 Java 编程语言中一个历史悠久的特性,它允许对象在被垃圾收集器回收前执行清理操作。然而,这个机制存在多个问题,包括性能开销、不确定性行为、死锁风险以及增加垃圾回收的复杂性。因此,Java 社区决定逐步淘汰这一特性。
弃用 Finalization 的原因主要包括:
- 性能问题:Finalization 会增加垃圾回收的停顿时间,因为它需要等待对象的
finalize()
方法执行完毕才能回收对象,这对于需要低延迟和高吞吐量的应用来说可能会产生性能问题 。 - 不确定性:Finalization 的执行时间是不确定的,因为它依赖于垃圾回收器的运行时机,而垃圾回收器的运行时机是不可预测的。这导致依赖
finalize()
进行资源清理的代码很难编写和调试 。 - 死锁风险:如果
finalize()
方法在执行过程中访问了其他对象,而这些对象又恰好正在被垃圾回收,那么就可能发生死锁 。 - 安全问题:Finalization 可以被恶意代码利用来破坏系统的安全性。例如,恶意代码可以覆盖对象的
finalize()
方法,在对象被垃圾回收时执行恶意操作 。
为了替代 Finalization,开发者被推荐使用 try-with-resources
语句或 java.lang.ref.Cleaner
类来管理资源。这些方法提供了更可靠和可预测的方式来释放资源 。
在 JDK 21 中,虽然 Finalization 仍然默认启用,但引入了一个新的命令行选项 --finalization=disabled
来禁用 Finalization。使用这个选项可以测试应用程序在没有 Finalization 的情况下的行为,以便于开发者评估他们的系统是否依赖于 Finalization 并进行必要的迁移 。
在未来的版本中将完全移除 Finalization 机制,并在 JDK 21 中标记所有相关的 API 为弃用。这包括 java.lang.Object.finalize()
方法以及其他几个在 java.base
和 java.desktop
模块中声明的终结器方法 。