JDK16特性
一、JAVA16概述
2021年3月16日正式发布,一共更新了17JEP
openjdk.java.net/projects/jd...
二、语法层面变化
1.JEP 397:密封类(第二次预览)
sealed class 第二次预览
通过密封的类和接口来增强Java编程语言,这是新的预览特性,用于限制超类的使用密封的类和接口限制其他可继承或者实现他们的其他类或接口.
目标
允许类或接口的开发者来控制那些代码负责实现,提供了比限制使用超类的访问修饰符声明方式更多选择,并通过支持对模式的详尽分析而支持模式匹配的未来发展
在java中,类层次构造通过集成实现代码的重用,父类的方法可以被许多子类继承.但是,类层次接口的目的并不总是重用代码.有时是对域中存在的各种可能性进行建模,例如图形库支持函的形状类型.当以这种方式使用类层次结构是,我们可能需要限制子类集从而简化建模.
虽然我们可以通过final来限定子类继承,但是这是绝对杜绝类子类,而类的密封是允许子类,但是限定是那个或者哪些.
2.JEP 394:instanceof 的模式匹配
概括
增强Java编程语言与模式匹配 的 instanceof
运算符。 模式匹配 允许更简洁、更安全地表达程序中的常见逻辑,即从对象中有条件地提取组件。
历史
模式匹配 instanceof
由JEP 305提出 并在 JDK 14 中作为 预览功能提供。它由JEP 375重新提出, 并在 JDK 15 中进行第二轮预览。
该 JEP 建议在 JDK 16 中完成该功能,并进行以下改进:
- 取消模式变量是隐式 final 的限制,以减少局部变量和模式变量之间的不对称性。
- 将
instanceof
类型S 的表达式与类型T 的模式进行比较,使模式表达式成为编译时错误,其中S 是T 的子类型。(这个instanceof
表达式总是成功,然后毫无意义。相反的情况,模式匹配总是失败,已经是一个编译时错误。)
可以根据进一步的反馈合并其他改进。
原因
几乎每个程序都包含某种逻辑,这些逻辑结合了测试表达式是否具有特定类型或结构,然后有条件地提取其状态的组件以进行进一步处理。例如,所有 Java 程序员都熟悉 instanceof
-and-cast 习语:
java
if (obj instanceof String) {
String s = (String) obj; // grr...
...
}
有三件事情会在这里:测试(是 obj
一 String
?),转换(铸造 obj
到 String
),和一个新的局部变量的声明(s
),这样我们就可以使用字符串值。这种模式很简单,所有 Java 程序员都可以理解,但由于几个原因,它并不是最理想的。它很乏味;应该不需要同时进行类型测试和强制转换(instanceof
测试后你还会做什么 ?)。这个样板------特别是该类型的三个出现 String
------混淆了后面更重要的逻辑。但最重要的是,重复提供了错误潜入程序中的机会。
我们相信 Java 是时候拥抱模式匹配了 ,而不是寻求临时解决方案。模式匹配允许简洁地表达对象的所需"形状"(模式 ),并允许各种语句和表达式根据其输入(匹配)测试该"形状" 。许多语言,从 Haskell 到 C#,都因为其简洁和安全而采用了模式匹配
这允许我们将上面繁琐的代码重构为以下内容:
java
if (obj instanceof String s) {
// Let pattern matching do the work!
...
}
3.JEP 395:记录
概述
使用记录增强 Java 编程语言,记录是充当不可变数据的透明载体的类。记录可以被认为是名义元组。
历史
记录由JEP 359提出 并在JDK 14 中作为 预览功能提供。
作为对反馈的回应,JEP 384对该设计进行了改进, 并在JDK 15 中作为第二次预览功能交付 。第二次预览的改进如下:
- 在第一个预览版中,规范构造函数必须是
public
. 在第二个预览中,如果隐式声明了规范构造函数,则其访问修饰符与记录类相同;如果显式声明了规范构造函数,则其访问修饰符必须提供至少与记录类一样多的访问权限。 @Override
注释的含义被扩展为包括注释方法是记录组件的显式声明的访问器方法的情况。- 为了强制使用紧凑构造函数,分配给构造函数主体中的任何实例字段会导致编译时错误。
- 引入了声明本地记录类、本地枚举类和本地接口的能力。
该 JEP 建议在 JDK 16 中完成该功能,并进行以下改进:
- 放宽长期存在的限制,即内部类不能声明显式或隐式静态成员。这将变得合法,特别是将允许内部类声明作为记录类的成员。
可以根据进一步的反馈合并其他改进。
目标
- 设计一个面向对象的构造来表达简单的值聚合。
- 帮助开发人员专注于建模不可变数据而不是可扩展行为。
- 自动实现数据驱动的方法,例如
equals
和访问器。 - 保留长期存在的 Java 原则,例如名义类型和迁移兼容性。
细节实现
人们普遍抱怨"Java 太冗长"或"仪式太多"。一些最严重的违规者是那些只不过是少数值的不可变 数据载体 的类。正确编写这样的数据载体类涉及许多低价值、重复、容易出错的代码:构造函数、访问器 equals
、hashCode
、toString
、 等。 例如,携带 x 和 y 坐标的类不可避免地以这样的方式结束:
arduino
class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
int x() { return x; }
int y() { return y; }
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point other = (Point) o;
return other.x == x && other.y == y;
}
public int hashCode() {
return Objects.hash(x, y);
}
public String toString() {
return String.format("Point[x=%d, y=%d]", x, y);
}
}
开发人员有时试图通过省略诸如 的方法来偷工减料 equals
,导致令人惊讶的行为或可调试性差,或者将替代但不完全合适的类压入服务中,因为它具有"正确的形状"并且他们还不想声明另一个班级。
集成开发环境帮助我们写 的大部分代码的数据载体类,但没有做任何事情来帮助读者 提炼出设计意图的"我是一个数据载体 x
和 y
"从几十个样板线。编写对少数值建模的 Java 代码应该更容易编写、阅读和验证是否正确。
虽然表面上将记录视为主要与样板减少有关,但我们选择了一个更具语义的目标:将数据建模为数据。(如果语义是正确的,样板将自行处理。)声明数据载体类应该简单而简洁*,默认情况下,* 这些类使它们的数据不可变,并提供生成和使用数据的方法的惯用实现。
记录类是 Java 语言中的一种新类。记录类有助于用比普通类更少的仪式对普通数据聚合进行建模。
记录类的声明主要由其状态的声明组成 ;然后记录类提交到与该状态匹配的 API。这意味着记录类放弃了类通常享有的自由------将类的 API 与其内部表示分离的能力------但作为回报,记录类声明变得更加简洁。
更准确地说,记录类声明由名称、可选类型参数、标题和正文组成。标题列出了记录类的组件 ,它们是构成其状态的变量。(此组件列表有时称为状态描述。)例如:
arduino
record Point(int x, int y) { }
因为记录类在语义上声称是其数据的透明载体,所以记录类会自动获取许多标准成员:
- 对于头部中的每个组件,两个成员:一个
public
与组件同名和返回类型的访问器方法,以及一个private
final
与组件类型相同的字段; - 一个规范构造函数,其签名与标头相同,并将每个私有字段分配给
new
实例化记录的表达式中的相应参数; equals
以及hashCode
确保两个记录值相同的方法,如果它们是相同的类型并且包含相同的组件值;和- 一种
toString
返回所有记录组件的字符串表示形式及其名称的方法。
换句话说,记录类的头部描述了它的状态,即它的组件的类型和名称,而 API 是从该状态描述中机械地和完全地派生出来的。API 包括用于构建、成员访问、平等和显示的协议。(我们希望未来的版本支持解构模式以实现强大的模式匹配。)
4_JEP 390:基于值的类的警告
概括
将原始包装类指定为基于值的,并弃用它们的构造函数以进行删除,提示新的弃用警告。提供有关对 Java 平台中任何基于值的类的实例进行同步的不当尝试的警告。
原因
在瓦尔哈拉项目正在推行显著增强了Java编程模型的形式原始类。这些类声明它们的实例是无身份的并且能够进行内联或扁平化表示,其中实例可以在内存位置之间自由复制并仅使用实例字段的值进行编码。
原始类的设计和实现已经足够成熟,我们可以自信地预期在未来的版本中将 Java 平台的某些类迁移为原始类。
迁移的候选者 在 API 规范中被非正式地指定为 基于值的类。从广义上讲,这意味着它们对标识对类的行为不重要的不可变对象进行编码,并且它们不提供实例创建机制,例如公共构造函数,保证每次调用具有唯一标识。
原始包装类(java.lang.Integer
、java.lang.Double
等)也旨在成为原始类。这些类满足大多数被指定为基于值的要求,但它们公开了不推荐使用的(自 Java 9 起)public
构造函数。通过对定义进行一些调整,它们也可以被视为基于值的类。
基于值的类的客户端通常不受原始类迁移的影响,除非它们违反了使用这些类的建议。特别是,在发生迁移的未来 Java 版本上运行时:
- 这些相等 (per
equals
) 的类的实例也可能被认为是相同的 (per==
),这可能会破坏依赖于!=
正确行为的结果的程序。 - 尝试使用
new Integer
,new Double
等创建包装类实例,而不是隐式装箱或对valueOf
工厂方法的调用,将产生LinkageError
。 - 尝试在这些类的实例上进行同步将产生异常。
这些更改对某些人来说可能不方便,但解决方法很简单:如果您需要标识,请使用不同的类------通常是您自己定义的类,但 Object
也 AtomicReference
可能是合适的。迁移到原始类的好处------更好的性能、可靠的相等语义、统一原始类和类------将非常值得带来的不便。
(1) 已经因为避免承诺基于值的类的工厂方法中的唯一身份而受到劝阻。没有一种实用的方法可以自动检测忽略这些规范并依赖当前实现行为的程序,但我们预计这种情况很少见。
我们可以通过弃用包装类构造函数来阻止(2)移除,这将放大编译对这些构造函数的调用时发生的警告。现有 Java 项目的很大一部分(可能占其中的 1%-10%)调用包装类构造函数,但在许多情况下,它们仅打算在 Java 9 之前的版本上运行。许多流行的开源项目已经通过从源代码中删除包装构造函数调用来响应 Java 9 的弃用警告,鉴于"弃用以删除"警告的紧迫性,我们可以期待更多这样做。用于缓解此问题的其他功能在依赖项部分中进行了描述。
我们可以通过在编译时和运行时实施警告来阻止 (3),以通知程序员他们的同步操作在未来版本中将不起作用。
三、API层面变化
1.JEP 338:Vector API(孵化器)
概述
提供的初始迭代培养箱模块, jdk.incubator.vector
来表达向量计算在运行时可靠地编译到最佳矢量的硬件指令上支持的CPU架构,从而实现优异的性能等效标量计算。
目标
- 清晰简洁的 API: API 应能够清晰简洁地表达广泛的矢量计算,这些矢量计算由一系列矢量操作组成,这些矢量操作通常在循环内组成,可能还有控制流。应该可以表达对向量大小(或每个向量的车道数)通用的计算,从而使此类计算能够在支持不同向量大小的硬件之间移植(如下一个目标中详述)。
- 平台不可知: API 应与体系结构无关,支持在支持向量硬件指令的多个 CPU 体系结构上的运行时实现。与平台优化和可移植性冲突的 Java API 中的常见情况一样,偏向于使 Vector API 具有可移植性,即使某些特定于平台的习语不能直接用可移植代码表达。x64 和 AArch64 性能的下一个目标代表了支持 Java 的所有平台上的适当性能目标。在ARM可缩放矢量扩展(SVE)在这方面特别关注,以确保API能支持这种架构,即使是写没有已知的生产硬件实现的。
- 在 x64 和 AArch64 架构上可靠的运行时编译和性能: Java 运行时,特别是 HotSpot C2 编译器,应在有能力的 x64 架构上将向量操作序列编译为相应的向量硬件指令序列,例如Streaming SIMD支持的那些 扩展(SSE) 和高级矢量扩展 (AVX) 扩展,从而生成高效和高性能的代码。程序员应该相信他们表达的向量操作将可靠地映射到相关的硬件向量指令。这同样适用于编译为Neon支持的向量硬件指令序列的有能力的 ARM AArch64 架构。
- 优雅降级: 如果向量计算无法在运行时完全表示为硬件向量指令序列,要么是因为架构不支持某些所需指令,要么是因为不支持另一种 CPU 架构,那么 Vector API 实现应优雅降级并且仍然起作用。这可能包括如果矢量计算无法充分编译为矢量硬件指令,则向开发人员发出警告。在没有向量的平台上,优雅降级将产生与手动展开循环竞争的代码,其中展开因子是所选向量中的通道数。
动机
Vector API 旨在通过提供一种在 Java 中编写复杂矢量算法的机制来解决这些问题,使用 HotSpot 中预先存在的矢量化支持,但使用用户模型使矢量化更加可预测和健壮。手工编码的向量循环可以表达 hashCode
自动向量化器可能永远不会优化的高性能算法(例如向量化或专门的数组比较)。这种显式矢量化 API 可能适用于许多领域,例如机器学习、线性代数、密码学、金融和 JDK 本身的用法。
四、其他变化
1.JEP 347:启用 C++14 语言功能
概括
允许在 JDK C++ 源代码中使用 C++14 语言特性,并给出关于哪些特性可以在 HotSpot 代码中使用的具体指导。
目标
通过 JDK 15,JDK 中 C++ 代码使用的语言特性已经被限制在 C++98/03 语言标准。在 JDK 11 中,代码已更新以支持使用较新版本的 C++ 标准进行构建,尽管它还没有使用任何新功能。这包括能够使用支持 C++11/14 语言功能的各种编译器的最新版本进行构建。
此 JEP 的目的是正式允许 JDK 中的 C++ 源代码更改以利用 C++14 语言功能,并提供有关哪些功能可以在 HotSpot 代码中使用的具体指导。
描述
要利用 C++14 语言功能,需要在构建时进行一些更改,具体取决于平台编译器。还需要指定各种平台编译器的最低可接受版本。应明确指定所需的语言标准;较新的编译器版本可能会默认使用较新的且可能不兼容的语言标准。
- Windows:JDK 11 需要 Visual Studio 2017。(早期版本会生成配置时警告,可能会也可能不起作用。)对于 Visual Studio 2017,默认的 C++ 标准是 C++14。
/std:c++14
应添加该选项。将完全放弃对旧版本的支持。 - Linux:将
-std=gnu++98
编译器选项替换为-std=c++14
. gcc 的最低支持版本是 5.0。 - macOS:将
-std=gnu++98
编译器选项替换为-std=c++14
. clang 的最低支持版本是 3.5。 - AIX/PowerPC:将
-std=gnu++98
编译器选项 替换为-std=c++14
并要求使用 xlclang++ 作为编译器。xlclang++ 的最低支持版本是 16.1。
2.JEP 357:从 Mercurial 迁移到 Git
概括
将 OpenJDK 社区的源代码存储库从 Mercurial (hg) 迁移到 Git。
目标
- 将所有单存储库 OpenJDK 项目从 Mercurial 迁移到 Git
- 保留所有版本控制历史,包括标签
- 根据 Git 最佳实践重新格式化提交消息
- 将jcheck、 webrev和 defpath工具移植到 Git
- 创建一个工具来在 Mercurial 和 Git 哈希之间进行转换
动机
迁移到 Git 的三个主要原因:
- 版本控制系统元数据的大小
- 可用工具
- 可用主机
转换后的存储库的初始原型显示版本控制元数据的大小显着减少。例如,存储库的 .git
目录对于 jdk/jdk
Git 大约为 300 MB,.hg
对于 Mercurial,该目录大约为 1.2 GB,具体取决于所使用的 Mercurial 版本。元数据的减少保留了本地磁盘空间并减少了克隆时间,因为需要通过线路的位更少。Git 还具有 仅克隆部分历史记录的浅层克隆,从而为不需要整个历史记录的用户提供更少的元数据。
与 Mercurial 相比,与 Git 交互的工具还有很多:
- 所有文本编辑器都具有 Git 集成,无论是本机还是插件形式,包括Emacs (magit插件)、Vim (fugitive.git插件)、 VS Code(内置)和 Atom(内置)。
- 几乎所有集成开发环境 (IDE) 还附带开箱即用的 Git 集成,包括 IntelliJ(内置)、 Eclipse(内置)、 NetBeans(内置)和 Visual Studio(内置)。
- 有多个桌面客户端可用于与本地 Git 存储库进行交互。
最后,有许多选项可用于托管 Git 存储库,无论是自托管还是作为服务托管。
3.JEP 376:ZGC:并发线程堆栈处理
概述
将 ZGC 线程堆栈处理从安全点移动到并发阶段。
目标
- 从 ZGC 安全点中删除线程堆栈处理。
- 使堆栈处理变得懒惰、协作、并发和增量。
- 从 ZGC 安全点中删除所有其他每线程根处理。
- 提供一种机制,其他 HotSpot 子系统可以通过该机制延迟处理堆栈。
原因
ZGC 垃圾收集器 (GC) 旨在使 HotSpot 中的 GC 暂停和可扩展性问题成为过去。到目前为止,我们已经将所有随堆大小和元空间大小扩展的 GC 操作从安全点操作移到并发阶段。这些包括标记、重定位、引用处理、类卸载和大多数根处理。
仍然在 GC 安全点中完成的唯一活动是根处理的子集和有时间限制的标记终止操作。根包括 Java 线程堆栈和各种其他线程根。这些根是有问题的,因为它们会随着线程的数量而扩展。由于大型机器上有许多线程,根处理成为一个问题。
为了超越我们今天所拥有的,并满足在 GC 安全点内花费的时间不超过一毫秒的期望,即使在大型机器上,我们也必须将这种每线程处理,包括堆栈扫描,移出并发阶段。
在这项工作之后,在 ZGC 安全点操作中基本上不会做任何重要的事情。
作为该项目的一部分构建的基础设施最终可能会被其他项目使用,例如 Loom 和 JFR,以统一延迟堆栈处理。
4.JEP 380:Unix 域套接字通道
概述
将 Unix 域 ( AF_UNIX
) 套接字支持添加到包中的套接字通道和服务器套接字通道API java.nio.channels
。扩展继承的通道机制以支持 Unix 域套接字通道和服务器套接字通道。
目标
Unix 域套接字用于同一主机上的进程间通信 (IPC)。它们在大多数方面类似于 TCP/IP 套接字,不同之处在于它们由文件系统路径名而不是 Internet 协议 (IP) 地址和端口号寻址。此 JEP 的目标是支持在主要 Unix 平台和 Windows 中通用的 Unix 域套接字的所有功能。Unix 域套接字通道在读/写行为、连接设置、服务器对传入连接的接受、与选择器中的其他非阻塞可选通道的多路复用以及相关套接字的支持方面的行为与现有的 TCP/IP 通道相同选项。
原因
对于本地、进程间通信,Unix 域套接字比 TCP/IP 环回连接更安全、更高效。
- Unix 域套接字严格用于同一系统上的进程之间的通信。不打算接受远程连接的应用程序可以通过使用 Unix 域套接字来提高安全性。
- Unix 域套接字受到操作系统强制的、基于文件系统的访问控制的进一步保护。
- Unix 域套接字比 TCP/IP 环回连接具有更快的设置时间和更高的数据吞吐量。
- 对于需要在同一系统上的容器之间进行通信的容器环境,Unix 域套接字可能是比 TCP/IP 套接字更好的解决方案。这可以使用位于共享卷中的套接字来实现。
Unix 域套接字长期以来一直是大多数 Unix 平台的一个功能,现在 Windows 10 和 Windows Server 2019 都支持。
具体操作
为了支持 Unix 域套接字通道,我们将添加以下 API 元素:
- 一个新的套接字地址类,
java.net.UnixDomainSocketAddress
; - 甲
UNIX
在现有的恒定值java.net.StandardProtocolFamily
枚举; - 新
open
的工厂方法SocketChannel
,并ServerSocketChannel
指定协议族 - 更新
SocketChannel
和ServerSocketChannel
规范以指定 Unix 域套接字的通道的行为方式。
5.JEP 386:Alpine Linux 端口
概括
将 JDK 移植到 Alpine Linux,以及在 x64 和 AArch64 架构上使用 musl 作为其主要 C 库的其他 Linux 发行版,
原因
Musl是针对基于 Linux 的系统的 ISO C 和 POSIX 标准中描述的标准库功能的实现。包括Alpine Linux和 OpenWrt在内的几个 Linux 发行版都基于 musl,而其他一些发行版提供了可选的 musl 包(例如Arch Linux)。
Alpine Linux 发行版由于其较小的镜像大小,被广泛用于云部署、微服务和容器环境。例如,用于 Alpine Linux 的Docker基础映像小于 6 MB。使 Java 在此类设置中开箱即用将允许 Tomcat、Jetty、Spring 和其他流行框架在此类环境中本地工作。
通过使用 jlink
(JEP 282)来减少 Java 运行时的大小,用户将能够创建一个更小的图像来运行特定的应用程序。应用程序所需的模块集可以通过 jdeps
命令确定。例如,如果目标应用程序仅依赖于 java.base
模块,则带有 Alpine Linux 的 Docker 映像和仅带有该模块的 Java 运行时和服务器 VM 大小为 38 MB。
同样的动机也适用于嵌入式部署,它们也有大小限制。
具体操作
该 JEP 旨在整合上游的Portola 项目。
此端口将不支持 HotSpot Serviceability Agent 的附加机制。
要在 Alpine Linux 上构建 JDK 的 musl 变体,需要以下软件包:
alpine-sdk alsa-lib alsa-lib-dev autoconf bash cups-dev cups-libs fontconfig fontconfig-dev freetype freetype-dev grep libx11 libx11-dev libxext libxext-dev libxrandr libxrandr-dev libxrender libxrender-dev libxt libxt-dev libxtst -dev linux-headers zip
安装这些软件包后,JDK 构建过程将照常工作。
如果有需求,可以在后续增强中实现其他架构的 Musl 端口。
6.JEP 387:弹性元空间
概括
更及时地将未使用的 HotSpot 类元数据(即元空间 )内存返还给操作系统,减少元空间占用空间,并简化元空间代码以降低维护成本。
原因
自从在JEP 122 中出现以来,元空间就因高堆外内存使用而臭名昭著。大多数普通应用程序没有问题,但很容易以错误的方式刺激元空间分配器,从而导致过多的内存浪费。不幸的是,这些类型的病例情况并不少见。
元空间内存在每类加载器管理领域。一个 arena 包含一个或多个 chunks,它的加载器通过廉价的指针碰撞从中分配。元空间块是粗粒度的,以保持分配操作的效率。然而,这会导致使用许多小类加载器的应用程序遭受不合理的高元空间使用。
当类加载器被回收时,其元空间领域中的块被放置在空闲列表中以供以后重用。然而,这种重用可能不会在很长一段时间内发生,或者可能永远不会发生。因此,具有大量类加载和卸载活动的应用程序可能会在元空间空闲列表中累积大量未使用的空间。如果没有碎片化,该空间可以返回给操作系统以用于其他目的,但通常情况并非如此。
具体操作
我们建议用基于好友的分配方案替换现有的元空间内存分配器。这是一种古老且经过验证的算法,已成功用于例如 Linux 内核。这种方案将使以更小的块分配元空间内存变得可行,这将减少类加载器的开销。它还将减少碎片,这将使我们能够通过将未使用的元空间内存返回给操作系统来提高弹性。
我们还将根据需要将操作系统中的内存延迟提交到 arenas。这将减少从大型竞技场开始但不立即使用它们或可能永远不会使用它们的全部范围的加载器的占用空间,例如引导类加载器。
最后,为了充分利用伙伴分配提供的弹性,我们将元空间内存安排成大小均匀的颗粒 ,这些颗粒可以相互独立地提交和取消提交。这些颗粒的大小可以通过一个新的命令行选项来控制,它提供了一种控制虚拟内存碎片的简单方法。
可以在此处找到详细描述新算法的文档。工作原型作为JDK 沙箱存储库中的一个分支存在。
7.JEP 388:Windows/AArch64 端口
概括
将 JDK 移植到 Windows/AArch64
动机
随着新的消费级和服务器级 AArch64 (ARM64) 硬件的发布,由于最终用户的需求,Windows/AArch64 已成为一个重要的平台。
具体操作
通过扩展之前为 Linux/AArch64 移植(JEP 237 )所做的工作,我们已将 JDK 移植到 Windows/AArch64 。此端口包括模板解释器、C1 和 C2 JIT 编译器以及垃圾收集器(串行、并行、G1、Z 和 Shenandoah)。它支持 Windows 10 和 Windows Server 2016 操作系统。
这个 JEP 的重点不是移植工作本身,它大部分是完整的,而是将移植集成到 JDK 主线存储库中。
目前,我们有十几个变更集。我们将对共享代码的更改保持在最低限度。我们的更改将 AArch64 内存模型的支持扩展到 Windows,解决了一些 MSVC 问题,将 LLP64 支持添加到 AArch64 端口,并在 Windows 上执行 CPU 功能检测。我们还修改了构建脚本以更好地支持交叉编译和 Windows 工具链。
新平台代码本身仅限于 15 (+4) 个文件和 1222 行 (+322)。
可在此处获得抢先体验的二进制文件。
8.JEP 389:外部链接器 API(孵化器)
概括
介绍一个 API,它提供对本机代码的静态类型、纯 Java 访问。此 API 与外部内存 API ( JEP 393 ) 一起,将大大简化绑定到本机库的其他容易出错的过程。
为该 JEP 提供基础的 Foreign-Memory Access API 最初由JEP 370提出,并于 2019 年底作为孵化 API面向 Java 14,随后由面向 Java 的JEP 383和JEP 393更新分别为 15 和 16。外部内存访问 API 和外部链接器 API 共同构成了巴拿马项目的关键可交付成果。
目标
- 易用性:用卓越的纯 Java 开发模型替换 JNI。
- C 支持: 这项工作的初始范围旨在在 x64 和 AArch64 平台上提供与 C 库的高质量、完全优化的互操作性。
- 通用性:外部链接器 API 和实现应该足够灵活,随着时间的推移,可以适应其他平台(例如,32 位 x86)和用非 C 语言编写的外部函数(例如 C++、Fortran)。
- 性能:外部链接器 API 应提供与 JNI 相当或优于 JNI 的性能。
原因
从 Java 1.1 开始,Java 就支持通过Java 本地接口 (JNI)调用本地方法,但这条路径一直是艰难而脆弱的。使用 JNI 包装本机函数需要开发多个工件:Java API、C 头文件和 C 实现。即使有工具帮助,Java 开发人员也必须跨多个工具链工作,以保持多个依赖于平台的工件同步。这对于稳定的 API 来说已经够难了,但是当试图跟踪正在进行的 API 时,每次 API 发展时更新所有这些工件是一个重大的维护负担。最后,JNI 主要是关于代码的,但代码总是交换数据,而 JNI 在访问本机数据方面提供的帮助很小。出于这个原因,开发人员经常求助于解决方法(例如直接缓冲区或 sun.misc.Unsafe
) 这使得应用程序代码更难维护,甚至更不安全。
多年来,出现了许多框架来填补 JNI 留下的空白,包括JNA、JNR和JavaCPP。JNA 和 JNR 从用户定义的接口声明动态生成包装器;JavaCPP 生成由 JNI 方法声明上的注释静态驱动的包装器。虽然这些框架通常比 JNI 体验有显着改进,但情况仍然不太理想,尤其是与提供一流的本地互操作的语言相比时。例如,Python 的ctypes包可以在没有任何胶水代码的情况下动态包装本机函数。其他语言,例如Rust,提供了从 C/C++ 头文件机械地派生本机包装器的工具。
最终,Java 开发人员应该能够(大部分)只使用任何被认为对特定任务有用的本地库------我们已经看到现状如何阻碍实现这一目标。此 JEP 通过引入高效且受支持的 API --- 外部链接器 API --- 来纠正这种不平衡,该 API 提供外部函数支持,而无需任何干预 JNI 胶水代码。它通过将外部函数公开为可以在纯 Java 代码中声明和调用的方法句柄来实现这一点。这大大简化了编写、构建和分发依赖于外部库的 Java 库和应用程序的任务。此外,Foreign Linker API 与 Foreign-Memory Access API 一起,为第三方本机互操作框架(无论是现在还是未来)都可以可靠地构建提供了坚实而高效的基础。
9.JEP 392:打包工具
概括
提供 jpackage
用于打包自包含 Java 应用程序的工具。
历史
该 jpackage
工具是由JEP 343在 JDK 14 中作为孵化工具引入的。它仍然是 JDK 15 中的一个孵化工具,以便有时间提供额外的反馈。现在可以将其从孵化提升为生产就绪功能。作为这种转换的结果,jpackage
模块的名称将从 更改 jdk.incubator.jpackage
为 jdk.jpackage
。
与 JEP 343 相关的唯一实质性变化是我们用 --bind-services
更通用的 --jlink-options
选项替换了该选项,如下所述。
目标
创建一个基于遗留 JavaFXjavapackager
工具的打包工具:
- 支持原生打包格式,为最终用户提供自然的安装体验。这些格式包括
msi
与exe
在Windows,pkg
并dmg
在MacOS,以及deb
和rpm
在Linux上。 - 允许在打包时指定启动时间参数。
- 可以直接从命令行调用,也可以通过
ToolProvider
API 以编程方式调用。
原因
许多 Java 应用程序需要以一流的方式安装在本机平台上,而不是简单地放置在类路径或模块路径上。应用程序开发人员提供一个简单的 JAR 文件是不够的;他们必须提供适合本机平台的可安装包。这允许以用户熟悉的方式分发、安装和卸载 Java 应用程序。例如,在 Windows 上,用户希望能够双击一个软件包来安装他们的软件,然后使用控制面板来删除软件;在 macOS 上,用户希望能够双击 DMG 文件并将他们的应用程序拖到应用程序文件夹中。
jpackage 工具还可以帮助填补过去技术留下的空白,例如从 Oracle 的 JDK 11 中删除的 Java Web Start,以及 pack200
在 JDK 14 ( JEP 367 ) 中删除的。开发人员可以 jlink
将 JDK 拆分为所需的最小模块集,然后使用打包工具生成可部署到目标机器的压缩、可安装映像。
以前,为了满足这些要求,javapackager
Oracle 的 JDK 8 随附了一个名为的打包工具。但是,作为 JavaFX 删除的一部分,它已从 Oracle 的 JDK 11 中删除。
具体操作
该 jpackage
工具将 Java 应用程序打包到特定于平台的包中,其中包含所有必需的依赖项。应用程序可以作为普通 JAR 文件的集合或作为模块的集合提供。支持的特定于平台的包格式是:
- Linux:
deb
和rpm
- macOS:
pkg
和dmg
- 窗户:
msi
和exe
默认情况下,jpackage
以最适合运行它的系统的格式生成包。
非模块化应用打包
假设您有一个由 JAR 文件组成的应用程序,所有这些文件都在一个名为 的目录中 lib
,并且 lib/main.jar
包含主类。然后命令
css
$ jpackage --name myapp --input lib --main-jar main.jar
将以本地系统的默认格式打包应用程序,将生成的包文件保留在当前目录中。如果 MANIFEST.MF
文件中 main.jar
没有 Main-Class
属性,则必须明确指定主类:
css
$ jpackage --name myapp --input lib --main-jar main.jar \
--main-class myapp.Main
包的名称将是 myapp
,但包文件本身的名称会更长,并以包类型结尾(例如,myapp.exe
)。该软件包将包含应用程序的启动器,也称为 myapp
. 为了启动应用程序,启动程序将从输入目录复制的每个 JAR 文件放置在 JVM 的类路径上。
如果您希望以默认格式以外的格式生成包,请使用该 --type
选项。例如,要在 macOS 上生成 pkg
文件而不是 dmg
文件:
css
$ jpackage --name myapp --input lib --main-jar main.jar --type pkg
模块化应用打包
如果您有一个模块化应用程序,由目录中的模块化 JAR 文件和/或 JMOD 文件组成,并且 lib
模块中的主类 myapp
,则命令
css
$ jpackage --name myapp --module-path lib -m myapp
会打包。如果 myapp
模块未标识其主类,那么您必须再次明确指定:
css
$ jpackage --name myapp --module-path lib -m myapp/myapp.Main
(创建模块化 JAR 或 JMOD 文件时,您可以使用和工具 --main-class
选项指定主类。)jar``jmod
10.JEP 393:外部内存访问 API(第三个孵化器)
概述
引入 API 以允许 Java 程序安全有效地访问 Java 堆之外的外部内存。
历史
Foreign-Memory Access API 最初由JEP 370提出,并于 2019 年底作为孵化 API面向 Java 14 ,后来由JEP 383重新孵化,后者在 2020 年中期面向 Java 15。该 JEP 建议结合基于反馈,并在 Java 16 中重新孵化 API。此 API 更新中包含以下更改:
MemorySegment
和MemoryAddress
接口之间的角色分离更清晰;- 一个新的接口,
MemoryAccess
它提供了通用的静态内存访问器,以便VarHandle
在简单的情况下最大限度地减少对API的需求; - 支持共享段;和
- 使用
Cleaner
.
目标
- *通用性:*单个 API 应该能够对各种外部内存(例如,本机内存、持久内存、托管堆内存等)进行操作。
- *安全性:*无论操作何种内存,API 都不应该破坏 JVM 的安全性。
- *控制:*客户端应该可以选择如何释放内存段:显式(通过方法调用)或隐式(当该段不再使用时)。
- *可用性:*对于需要访问外部内存的程序,API 应该是传统 Java API(如
sun.misc.Unsafe
.
原因
许多 Java 程序访问外部内存,例如Ignite、mapDB、memcached、Lucene和 Netty 的ByteBuf API。通过这样做,他们可以:
- 避免与垃圾收集相关的成本和不可预测性(尤其是在维护大型缓存时);
- 跨多个进程共享内存;和
- 通过将文件映射到内存(例如,通过
mmap
)来序列化和反序列化内存内容。
不幸的是,Java API 并没有为访问外部内存提供令人满意的解决方案:
- Java 1.4 中引入的
ByteBuffer
API允许创建直接 字节缓冲区,这些缓冲区在堆外分配,因此允许用户直接从 Java 操作堆外内存。但是,直接缓冲区是有限的。例如,无法创建大于 2 GB 的直接缓冲区,因为ByteBuffer
API 使用int
基于索引的方案。此外,使用直接缓冲区可能很麻烦,因为与它们相关联的内存的释放留给垃圾收集器;也就是说,只有在垃圾收集器认为直接缓冲区不可访问后,才能释放相关内存。多年来,为了克服这些和其他限制(例如4496703、6558368,4837564和5029431)。许多这些限制源于这样一个事实,即ByteBuffer
API 不仅设计用于堆外内存访问,而且还用于生产者/消费者在字符集编码/解码和部分 I/O 操作等领域交换批量数据。 - 开发人员可以从 Java 代码访问外部内存的另一个常见途径是
Unsafe
API。由于相对通用的寻址模型Unsafe
,公开了许多适用于堆上和堆外访问的内存访问操作(例如,Unsafe::getInt
和putInt
)。使用Unsafe
访问内存是非常有效的:所有的内存访问操作被定义为热点JVM内部函数,所以存储器存取操作是由热点JIT编译器优化常规。然而,Unsafe
根据定义,API 是不安全的 ------它允许访问任何 内存位置(例如,Unsafe::getInt
获取long
地址)。这意味着 Java 程序可以通过访问一些已经释放的内存位置来使 JVM 崩溃。最重要的是,Unsafe
API 不是受支持的 Java API,一直强烈不鼓励使用它。 - 使用 JNI 访问外部内存是一种可能,但与此解决方案相关的固有成本使其在实践中很少适用。整个开发流程很复杂,因为 JNI 要求开发人员编写和维护 C 代码片段。JNI 本身也很慢,因为每次访问都需要从 Java 到本机的转换。
总之,在访问外部内存时,开发人员面临着两难选择:他们应该选择安全但有限(并且可能效率较低)的路径,例如 ByteBuffer
API,还是应该放弃安全保证并接受危险且不受支持的路径?Unsafe
API?
此 JEP 为外部内存访问引入了安全、受支持且高效的 API。通过为访问外部内存的问题提供有针对性的解决方案,开发人员将摆脱现有 API 的限制和危险。他们还将享受更高的性能,因为新的 API 将在设计时考虑到 JIT 优化。
具体操作
Foreign-Memory Access API 作为一个名为的孵化器模块提供 jdk.incubator.foreign
,位于同名的包中。它引入了三个主要抽象:MemorySegment
,MemoryAddress
和 MemoryLayout
:
MemorySegment
对具有给定空间和时间界限的连续内存区域进行建模,- `MemoryAddress对一个地址建模,该地址可以驻留在堆上或堆外,并且
MemoryLayout
是对内存段内容的编程描述。
可以从各种来源创建内存段,例如本机内存缓冲区、内存映射文件、Java 数组和字节缓冲区(直接或基于堆)。例如,可以按如下方式创建本机内存段:
ini
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
...
}
这将创建一个与大小为 100 字节的本机内存缓冲区相关联的内存段。
内存段在空间上是有界的,这意味着它们有下限和上限。任何尝试使用该段访问这些边界之外的内存都将导致异常。
正如 try
上面使用-with-resource 构造所证明的那样,内存段也是有时间限制的,这意味着它们必须被创建、使用,然后在不再使用时关闭。关闭一个段会导致额外的副作用,例如与该段相关联的内存的释放。任何访问已关闭内存段的尝试都会导致异常。空间和时间边界共同保证了外部内存访问 API 的安全性,从而保证它的使用不会使 JVM 崩溃。
11.JEP 396:默认情况下强封装 JDK 内部
概述
默认情况下,强封装 JDK 的所有内部元素,除了关键的内部 API,如 sun.misc.Unsafe
. 允许最终用户选择自 JDK 9 以来一直默认的宽松强封装。
目标
- 继续提高 JDK 的安全性和可维护性,这是Project Jigsaw的主要目标之一。
- 鼓励开发人员从使用内部元素迁移到使用标准 API,以便他们和他们的用户可以轻松升级到未来的 Java 版本。
动机
多年来,各种库、框架、工具和应用程序的开发人员以损害安全性和可维护性的方式使用 JDK 的内部元素。特别是:
- 包的一些非
public
类、方法和字段java.*
定义了特权操作,例如在特定类加载器中定义新类的能力,而其他则传送敏感数据,例如加密密钥。尽管在java.*
包中,但这些元素是 JDK 的内部元素。外部代码通过反射使用这些内部元素会使平台的安全性处于危险之中。 - 包的所有类、方法和字段
sun.*
都是 JDK 的内部 API。一些类,方法和属性com.sun.*
,jdk.*
以及org.*
包也是内部API。这些 API 从来都不是标准的,从来没有得到支持,也从来没有打算供外部使用。外部代码使用这些内部元素是一种持续的维护负担。为了不破坏现有代码,保留这些 API 所花费的时间和精力可以更好地用于推动平台向前发展。
在 Java 9 中,我们通过利用模块来限制对其内部元素的访问,提高了 JDK 的安全性和可维护性。模块提供强封装,这意味着
- 模块外的代码只能访问该 模块导出的包的
public
和protected
元素,并且 protected
此外,元素只能从定义它们的类的子类中访问。
强封装适用于编译时和运行时,包括编译代码尝试在运行时通过反射访问元素时。public
导出包的非元素和未导出包的所有元素都被称为强封装。
在 JDK 9 及更高版本中,我们强烈封装了所有新的内部元素,从而限制了对它们的访问。然而,为了帮助迁移,我们故意选择不在运行时强封装 JDK 8 中存在的包的内容。因此类路径上的库和应用程序代码可以继续使用反射来访问非 public
元素的 java.*
包,以及所有元素 sun.*
和其他内部包,用于封装在JDK 8存在这种安排被称为放松强大的封装性。
我们于 2017 年 9 月发布了 JDK 9。 JDK 的大多数常用内部元素现在都有标准替代品。开发人员必须在三年中,从标准的API,如JDK的内部元素迁移走 java.lang.invoke.MethodHandles.Lookup::defineClass
,java.util.Base64
和 java.lang.ref.Cleaner
。许多库、框架和工具维护者已经完成了迁移并发布了其组件的更新版本。我们现在准备迈出下一步,按照 sun.misc.Unsafe
Project Jigsaw 最初计划的那样,对 JDK 的所有内部元素进行强封装------除了关键的内部 API,例如。
具体操作
松弛强封装由启动器选项控制 --illegal-access
。这个选项由JEP 261引入,被挑衅地命名以阻止它的使用。它目前的工作原理如下:
-
--illegal-access=permit
安排 JDK 8 中存在的每个包都对未命名模块中的代码开放。因此,类路径上的代码可以继续使用反射来访问包的非公共元素java.*
,以及sun.*
JDK 8 中存在的包和其他内部包的所有元素。对任何此类元素的第一次反射访问操作会导致发出警告,但在那之后不会发出警告。自 JDK 9 以来,此模式一直是默认模式。
-
--illegal-access=warn``permit
除了针对每个非法反射访问操作发出警告消息之外,其他都相同。 -
--illegal-access=debug``warn
除了为每个非法反射访问操作发出警告消息和堆栈跟踪之外,其他都相同。 -
--illegal-access=deny
禁用所有非法访问操作,但由其他命令行选项启用的操作除外,例如 ,--add-opens
。
作为对 JDK 的所有内部元素进行强封装的下一步,我们建议将 --illegal-access
选项的默认模式从 permit
更改为 deny
。通过此更改,默认情况下将不再打开JDK 8 中存在且不包含关键内部 API 的包;此处提供完整列表 。 该 sun.misc
包仍将由 jdk.unsupported
模块导出,并且仍可通过反射访问。
我们还将修改Java 平台规范中的相关文本,以禁止在任何 Java 平台实现中默认打开任何包,除非该包明确声明 open
在其包含模块的声明中。
的 permit
,warn
以及 debug
该模式 --illegal-access
选项将继续工作。这些模式允许最终用户根据需要选择宽松的强封装。
我们预计未来的 JEP 会 --illegal-access
完全取消该选项。那时将无法通过单个命令行选项打开所有 JDK 8 包。仍然可以使用--add-opens
命令行选项或 Add-Opens
JAR-file 属性来打开特定的包。
为了准备最终删除该 --illegal-access
选项,我们将弃用它作为本 JEP 的一部分进行删除。因此,为 java
启动器指定该选项将导致发出弃用警告。
JDK17特性
一、JAVA17概述
JDK 16 刚发布半年(2021/03/16),JDK 17 又如期而至(2021/09/14),这个时间点特殊,蹭苹果发布会的热度?记得当年 JDK 15 的发布也是同天
Oracle 宣布,从 JDK 17 开始,后面的 JDK 都全部免费提供!!!
Java 17+ 可以免费使用了,包括商用,更详细的条款可以阅读:
JDK 17 是自 2018 年 JDK 11 后的第二个长期支持版本,支持到 2029 年 9 月,支持时间长达 8 年,这下可以不用死守 JDK 8 了,JDK 17+ 也可以是一种新的选择了。下一个第三个长期支持版本是 JDK 21,时间为 2023 年 9 月,这次长期支持版本发布计划改了,不再是原来的 3 年一次,而是改成了 2 年一次!非长期支持版本还是半年发一次不变,下一个非长期支持版本计划在 2022/03 发布
OpenJDK文档:openjdk.java.net/projects/jd...
JDK 17 这个版本提供了 14 个增强功能,另外在性能、稳定性和安全性上面也得到了大量的提升,以及还有一些孵化和预览特性,有了这些新变化,Java 会进一步提高开发人员的生产力。
二、语法层面的变化
1.JEP 409:密封类
概述
密封类,这个特性在 JDK 15 中首次成为预览特性,在 JDK 16 中进行二次预览,在 JDK 17 这个版本中终于正式转正了。
历史
密封类是由JEP 360提出的,并在JDK 15 中作为 预览功能提供。它们由JEP 397再次提出并进行了改进,并作为预览功能在 JDK 16中提供。该 JEP 建议在 JDK 17 中完成密封类,与 JDK 16 没有任何变化。
目标
原因
类和接口的继承层次结构的面向对象数据模型已被证明在对现代应用程序处理的现实世界数据进行建模方面非常有效。这种表现力是 Java 语言的一个重要方面。
然而,在某些情况下,可以有效地控制这种表现力。例如,Java 支持枚举类 来模拟给定类只有固定数量实例的情况。在以下代码中,枚举类列出了一组固定的行星。它们是该类的唯一值,因此您可以彻底切换它们------无需编写 default
子句:
java
enum Planet { MERCURY, VENUS, EARTH }
Planet p = ...
switch (p) {
case MERCURY: ...
case VENUS: ...
case EARTH: ...
}
使用枚举类值的模型固定集通常是有帮助的,但有时我们想一套固定的模型种价值。我们可以通过使用类层次结构来做到这一点,而不是作为代码继承和重用的机制,而是作为列出各种值的一种方式。以我们的行星示例为基础,我们可以对天文领域中的各种值进行建模,如下所示:
kotlin
interface Celestial { ... }
final class Planet implements Celestial { ... }
final class Star implements Celestial { ... }
final class Comet implements Celestial { ... }
然而,这种层次结构并没有反映重要的领域知识,即我们的模型中只有三种天体。在这些情况下,限制子类或子接口的集合可以简化建模。
考虑另一个例子:在一个图形库中,一个类的作者 Shape
可能希望只有特定的类可以扩展 Shape
,因为该库的大部分工作涉及以适当的方式处理每种形状。作者对处理 的已知子类的代码的清晰度感兴趣 Shape
,而对编写代码来防御 的未知子类不感兴趣 Shape
。允许任意类扩展 Shape
,从而继承其代码以供重用,在这种情况下不是目标。不幸的是,Java 假定代码重用始终是一个目标:如果 Shape
可以扩展,那么它可以扩展任意数量的类。放宽这个假设是有帮助的,这样作者就可以声明一个类层次结构,该层次结构不能被任意类扩展。在这样一个封闭的类层次结构中,代码重用仍然是可能的,但不能超出。
Java 开发人员熟悉限制子类集的想法,因为它经常出现在 API 设计中。该语言在这方面提供了有限的工具:要么创建一个类 final
,使其具有零个子类,要么使该类或其构造函数成为包私有的,因此它只能在同一个包中具有子类。包私有超类的示例 出现在 JDK 中:
scala
package java.lang;
abstract class AbstractStringBuilder { ... }
public final class StringBuffer extends AbstractStringBuilder { ... }
public final class StringBuilder extends AbstractStringBuilder { ... }
当目标是代码重用时,包私有方法很有用,例如 AbstractStringBuilder
让 append
. 然而,当目标是对替代方案进行建模时,这种方法是无用的,因为用户代码无法访问关键抽象------超类------以 switch
覆盖它。允许用户访问超类而不允许他们扩展它是无法指定的,除非诉诸涉及非 public
构造函数的脆弱技巧------这些技巧不适用于接口。在声明 Shape
及其子类的图形库中,如果只有一个包可以访问 Shape
.
总之,超类应该可以被广泛访问 (因为它代表用户的重要抽象)但不能广泛 扩展 (因为它的子类应该仅限于作者已知的那些)。这样一个超类的作者应该能够表达它是与一组给定的子类共同开发的,既为读者记录意图,又允许 Java 编译器强制执行。同时,超类不应过度约束其子类,例如,强迫它们成为 final
或阻止它们定义自己的状态。
2.JEP 406:switch模式匹配(预览)
概述
使用 switch
表达式和语句的模式匹配以及对模式语言的扩展来增强 Java 编程语言。扩展模式匹配以 switch
允许针对多个模式测试表达式,每个模式都有特定的操作,以便可以简洁安全地表达复杂的面向数据的查询。
instanceof 模式匹配是JAVA14 非常赞的一个新特性! 这次在 JDK 17 中为 switch 语句支持模式匹配
目标
switch
通过允许模式出现在case
标签中来扩展表达式和语句的表现力和适用性。- 允许在
switch
需要时放松对历史的零敌意。 - 引入两种新的模式:保护模式 ,允许使用任意布尔表达式细化模式匹配逻辑,以及带 括号的模式,以解决一些解析歧义。
- 确保所有现有的
switch
表达式和语句继续编译而不做任何更改并以相同的语义执行。 - 不要引入
switch
与传统switch
构造分离的具有模式匹配语义的新式表达式或语句。 switch
当 case 标签是模式与 case 标签是传统常量时,不要使表达式或语句的行为不同。
老式的写法
java
static String formatter(Object o) {
String formatted = "unknown";
if (o instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
formatted = String.format("double %f", d);
} else if (o instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
支持模式匹配的switch
java
static String formatterPatternSwitch(Object o) {
return 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();
};
}
直接在 switch 上支持 Object 类型,这就等于同时支持多种类型,使用模式匹配得到具体类型,大大简化了语法量,这个功能还是挺实用的, 目前看转正只是一个时间上的问题而已.
三、API层面变化
1.JEP 414:Vector API(第二个孵化器)
概括
引入一个 API 来表达向量计算,这些计算在运行时可靠地编译为支持的 CPU 架构上的最佳向量指令,从而实现优于等效标量计算的性能。
历史
Vector API 由JEP 338提出并作为孵化 API集成到 Java 16 中。我们在此建议结合改进以响应反馈以及性能改进和其他重要的实施改进。我们包括以下显着变化:
- 增强 API 以支持字符操作,例如 UTF-8 字符解码。具体来说,我们添加了在
short
向量和char
数组之间复制字符的方法,以及用于与整数向量进行无符号比较的新向量比较运算符。 - 用于将
byte
向量与boolean
数组相互转换的 API 的增强功能。 - 使用英特尔的短向量数学库 (SVML)对 x64 上的超越和三角泳道运算的内在支持。
- Intel x64 和 ARM NEON 实现的一般性能增强。
目标
- 清晰简洁的 API ------API 应该能够清晰简洁地表达广泛的向量计算,这些向量计算由循环内组成的向量操作序列组成,可能还有控制流。应该可以表达与向量大小或每个向量的车道数相关的通用计算,从而使此类计算能够跨支持不同向量大小的硬件进行移植。
- 平台不可知------API应该是 CPU 架构不可知的,支持在支持向量指令的多种架构上实现。就像在 Java API 中一样,平台优化和可移植性发生冲突,那么偏向于使 API 可移植,即使这会导致某些特定于平台的习语无法在可移植代码中表达。
- 在 x64 和 AArch64 架构上可靠的运行时编译和性能 ------在强大的 x64 架构上,Java 运行时,特别是 HotSpot C2 编译器,应该将向量操作编译为相应的高效和高性能向量指令,例如那些由Streaming SIMD Extensions (SSE) 和Advanced支持的 指令矢量扩展 (AVX)。开发人员应该相信他们表达的向量操作将可靠地映射到相关的向量指令。在功能强大的 ARM AArch64 架构上,C2 将类似地将向量操作编译为NEON支持的向量指令。
- 优雅降级------有时向量计算无法在运行时完全表达为向量指令序列,这可能是因为架构不支持某些所需的指令。在这种情况下,Vector API 实现应该优雅地降级并仍然起作用。如果无法将向量计算有效地编译为向量指令,则这可能涉及发出警告。在没有向量的平台上,优雅降级将产生与手动展开循环竞争的代码,其中展开因子是所选向量中的通道数。
原因
向量计算由对向量的一系列操作组成。向量包含(通常)固定的标量值序列,其中标量值对应于硬件定义的向量通道的数量。应用于具有相同通道数的两个向量的二元运算,对于每个通道,将对来自每个向量的相应两个标量值应用等效的标量运算。这通常称为单指令多数据(SIMD)。
向量运算表达了一定程度的并行性,可以在单个 CPU 周期内执行更多工作,从而显着提高性能。例如,给定两个向量,每个向量包含八个整数的序列(即八个通道),可以使用单个硬件指令将这两个向量相加。向量加法指令对十六个整数进行运算,执行八次整数加法,而通常对两个整数进行运算,执行一次整数加法所需的时间。
HotSpot 已经支持自动向量化,它将标量操作转换为超字操作,然后映射到向量指令。可转换的标量操作集是有限的,并且在代码形状的变化方面也很脆弱。此外,可能仅使用可用向量指令的子集,从而限制了生成代码的性能。
今天,希望编写可靠地转换为超字操作的标量操作的开发人员需要了解 HotSpot 的自动矢量化算法及其局限性,以实现可靠和可持续的性能。在某些情况下,可能无法编写可转换的标量操作。例如,HotSpot 不会转换用于计算数组散列码的简单标量操作(因此是 Arrays::hashCode
方法),也不能自动矢量化代码以按字典顺序比较两个数组(因此我们添加了用于字典比较的内在函数)。
Vector API 旨在通过提供一种在 Java 中编写复杂矢量算法的方法来改善这种情况,使用现有的 HotSpot 自动矢量化器,但使用用户模型使矢量化更加可预测和健壮。手工编码的向量循环可以表达高性能算法,例如向量化 hashCode
或专门的数组比较,自动向量化器可能永远不会优化这些算法。许多领域都可以从这个显式向量 API 中受益,包括机器学习、线性代数、密码学、金融和 JDK 本身的代码。
2.JEP 415:特定于上下文的反序列化过滤器
概括
允许应用程序通过 JVM 范围的过滤器工厂配置特定于上下文和动态选择的反序列化过滤器,该工厂被调用以为每个单独的反序列化操作选择一个过滤器
原因
反序列化不受信任的数据是一种固有的危险活动,因为传入数据流的内容决定了创建的对象、其字段的值以及它们之间的引用。在许多典型用途中,流中的字节是从未知、不受信任或未经身份验证的客户端接收的。通过仔细构建流,攻击者可以导致恶意执行任意类中的代码。如果对象构造具有改变状态或调用其他操作的副作用,那么这些操作可能会损害应用程序对象、库对象甚至 Java 运行时的完整性。禁用反序列化攻击的关键是防止任意类的实例被反序列化,从而防止直接或间接执行它们的方法。
我们在 Java 9 中引入了反序列化过滤器 (JEP 290),使应用程序和库代码能够在反序列化之前验证传入的数据流。此类代码java.io.ObjectInputFilter
在创建反序列化流(即 a java.io.ObjectInputStream
)时提供验证逻辑作为 a 。
依赖流的创建者来明确请求验证有几个限制。这种方法不能扩展,并且很难在代码发布后更新过滤器。它也不能对应用程序中第三方库执行的反序列化操作进行过滤。
为了解决这些限制,JEP 290 还引入了一个 JVM 范围的反序列化过滤器,可以通过 API、系统属性或安全属性进行设置。此过滤器是*静态的,*因为它在启动时只指定一次。使用静态 JVM 范围过滤器的经验表明,它也有局限性,尤其是在具有库层和多个执行上下文的复杂应用程序中。对每个使用 JVM 范围的过滤器 ObjectInputStream
要求过滤器覆盖应用程序中的每个执行上下文,因此过滤器通常会变得过于包容或过于严格。
更好的方法是以不需要每个流创建者参与的方式配置每个流过滤器。
为了保护 JVM 免受反序列化漏洞的影响,应用程序开发人员需要清楚地描述每个组件或库可以序列化或反序列化的对象。对于每个上下文和用例,开发人员应该构建并应用适当的过滤器。例如,如果应用程序使用特定库来反序列化特定对象群组,则可以在调用库时应用相关类的过滤器。创建类的允许列表并拒绝其他所有内容,可以防止流中未知或意外的对象。封装或其他自然应用程序或库分区边界可用于缩小允许或绝对不允许的对象集。
应用程序的开发人员处于了解应用程序组件的结构和操作的最佳位置。此增强功能使应用程序开发人员能够构建过滤器并将其应用于每个反序列化操作。
具体操作
如上所述,JEP 290 引入了 per-stream 反序列化过滤器和静态 JVM-wide 过滤器。每当 ObjectInputStream
创建一个时,它的每个流过滤器都会被初始化为静态 JVM 范围的过滤器。如果需要,可以稍后将每个流过滤器更改为不同的过滤器。
这里我们介绍一个可配置的 JVM 范围的过滤器工厂 。每当 ObjectInputStream
创建an 时 ,它的每个流过滤器都会初始化为通过调用静态 JVM 范围过滤器工厂返回的值。因此,这些过滤器是动态的 和特定 于上下文的,与单个静态 JVM 范围的反序列化过滤器不同。为了向后兼容,如果未设置过滤器工厂,则内置工厂将返回静态 JVM 范围的过滤器(如果已配置)。
过滤器工厂用于 Java 运行时中的每个反序列化操作,无论是在应用程序代码、库代码中,还是在 JDK 本身的代码中。该工厂特定于应用程序,应考虑应用程序中的每个反序列化执行上下文。过滤器工厂从 ObjectInputStream
构造函数调用,也从 ObjectInputStream.setObjectInputFilter
. 参数是当前过滤器和新过滤器。从构造函数调用时,当前过滤器是 null
,新过滤器是静态 JVM 范围的过滤器。工厂确定并返回流的初始过滤器。工厂可以使用其他特定于上下文的控件创建复合过滤器,或者只返回静态 JVM 范围的过滤器。如果 ObjectInputStream.setObjectInputFilter
被调用,工厂被第二次调用,并使用第一次调用返回的过滤器和请求的新过滤器。工厂决定如何组合两个过滤器并返回过滤器,替换流上的过滤器。
对于简单的情况,过滤器工厂可以为整个应用程序返回一个固定的过滤器。例如,这里有一个过滤器,它允许示例类,允许 java.base
模块中的类,并拒绝所有其他类:
ini
var filter = ObjectInputFilter.Config.createFilter("example.*;java.base/*;!*")
在具有多个执行上下文的应用程序中,过滤器工厂可以通过为每个上下文提供自定义过滤器来更好地保护各个上下文。构建流时,过滤器工厂可以根据当前线程本地状态、调用者的层次结构、库、模块和类加载器来识别执行上下文。此时,用于创建或选择过滤器的策略可以根据上下文选择特定过滤器或过滤器组合。
如果存在多个过滤器,则可以合并它们的结果。组合过滤器的一种有用方法是,如果任何过滤器拒绝反序列化,则拒绝反序列化,如果任何过滤器允许,则允许反序列化,否则保持未定状态。
命令行使用
属性 jdk.serialFilter
和 jdk.serialFilterFactory
可以 在命令行上设置过滤器和过滤器工厂。现有 jdk.serialFilter
属性设置基于模式的过滤器。
该 jdk.serialFilterFactory
属性是在第一次反序列化之前要设置的过滤器工厂的类名。该类必须是公共的,并且可由应用程序类加载器访问。
为了与 JEP 290 兼容,如果 jdk.serialFilterFactory
未设置属性,则过滤器工厂将设置为提供与早期版本兼容的内置函数。
应用程序接口
我们在 ObjectInputFilter.Config
类中定义了两个方法来设置和获取 JVM 范围的过滤器工厂。过滤器工厂是一个有两个参数的函数,一个当前过滤器和一个下一个过滤器,它返回一个过滤器。
java
/**
* Return the JVM-wide deserialization filter factory.
*
* @return the JVM-wide serialization filter factory; non-null
*/
public static BinaryOperator<ObjectInputFilter> getSerialFilterFactory();
/**
* Set the JVM-wide deserialization filter factory.
*
* The filter factory is a function of two parameters, the current filter
* and the next filter, that returns the filter to be used for the stream.
*
* @param filterFactory the serialization filter factory to set as the
* JVM-wide filter factory; not null
*/
public static void setSerialFilterFactory(BinaryOperator<ObjectInputFilter> filterFactory);
示例
这个类展示了如何过滤到当前线程中发生的每个反序列化操作。它定义了一个线程局部变量来保存每个线程的过滤器,定义一个过滤器工厂来返回该过滤器,将工厂配置为 JVM 范围的过滤器工厂,并提供一个实用函数来 Runnable
在特定的 per 上下文中运行-线程过滤器。
java
public class FilterInThread implements BinaryOperator<ObjectInputFilter> {
// ThreadLocal to hold the serial filter to be applied
private final ThreadLocal<ObjectInputFilter> filterThreadLocal = new ThreadLocal<>();
// Construct a FilterInThread deserialization filter factory.
public FilterInThread() {}
/**
* The filter factory, which is invoked every time a new ObjectInputStream
* is created. If a per-stream filter is already set then it returns a
* filter that combines the results of invoking each filter.
*
* @param curr the current filter on the stream
* @param next a per stream filter
* @return the selected filter
*/
public ObjectInputFilter apply(ObjectInputFilter curr, ObjectInputFilter next) {
if (curr == null) {
// Called from the OIS constructor or perhaps OIS.setObjectInputFilter with no current filter
var filter = filterThreadLocal.get();
if (filter != null) {
// Prepend a filter to assert that all classes have been Allowed or Rejected
filter = ObjectInputFilter.Config.rejectUndecidedClass(filter);
}
if (next != null) {
// Prepend the next filter to the thread filter, if any
// Initially this is the static JVM-wide filter passed from the OIS constructor
// Append the filter to reject all UNDECIDED results
filter = ObjectInputFilter.Config.merge(next, filter);
filter = ObjectInputFilter.Config.rejectUndecidedClass(filter);
}
return filter;
} else {
// Called from OIS.setObjectInputFilter with a current filter and a stream-specific filter.
// The curr filter already incorporates the thread filter and static JVM-wide filter
// and rejection of undecided classes
// If there is a stream-specific filter prepend it and a filter to recheck for undecided
if (next != null) {
next = ObjectInputFilter.Config.merge(next, curr);
next = ObjectInputFilter.Config.rejectUndecidedClass(next);
return next;
}
return curr;
}
}
/**
* Apply the filter and invoke the runnable.
*
* @param filter the serial filter to apply to every deserialization in the thread
* @param runnable a Runnable to invoke
*/
public void doWithSerialFilter(ObjectInputFilter filter, Runnable runnable) {
var prevFilter = filterThreadLocal.get();
try {
filterThreadLocal.set(filter);
runnable.run();
} finally {
filterThreadLocal.set(prevFilter);
}
}
}
如果已经设置了特定于流的过滤器, ObjectInputStream::setObjectFilter
则过滤器工厂将该过滤器与下一个过滤器组合。如果任一过滤器拒绝一个类,则该类将被拒绝。如果任一过滤器允许该类,则该类被允许。否则,结果未定。
这是使用 FilterInThread
该类的一个简单示例:
java
// Create a FilterInThread filter factory and set
var filterInThread = new FilterInThread();
ObjectInputFilter.Config.setSerialFilterFactory(filterInThread);
// Create a filter to allow example.* classes and reject all others
var filter = ObjectInputFilter.Config.createFilter("example.*;java.base/*;!*");
filterInThread.doWithSerialFilter(filter, () -> {
byte[] bytes = ...;
var o = deserializeObject(bytes);
});
四、其他变化
1.JEP 306:恢复始终严格的浮点语义
概括
使浮点运算始终严格,而不是同时具有严格的浮点语义 ( strictfp
) 和略有不同的默认浮点语义。这将恢复语言和 VM 的原始浮点语义,匹配 Java SE 1.2 中引入严格和默认浮点模式之前的语义。
目标
- 简化数字敏感库的开发,包括
java.lang.Math
和java.lang.StrictMath
. - 在平台的一个棘手方面提供更多的规律性。
原因
1990 年代后期改变平台默认浮点语义的动力源于原始 Java 语言和 JVM 语义之间的不良交互以及流行的 x86 架构的 x87 浮点协处理器指令集的一些不幸特性. 在所有情况下匹配精确的浮点语义,包括非正规操作数和结果,需要大量额外指令的开销。在没有上溢或下溢的情况下匹配结果可以用更少的开销来实现,这大致是 Java SE 1.2 中引入的修改后的默认浮点语义所允许的。
但是,从 2001 年左右开始在奔腾 4 和更高版本的处理器中提供的 SSE2(流式 SIMD 扩展 2)扩展可以以直接的方式支持严格的 JVM 浮点运算,而不会产生过多的开销。
由于英特尔和 AMD 长期以来都支持 SSE2 和更高版本的扩展,这些扩展允许自然支持严格的浮点语义,因此不再存在具有不同于严格的默认浮点语义的技术动机。
具体细节描述
此 JEP 将修改的接口包括浮点表达式覆盖范围内的 Java 语言规范(请参阅JLS部分 4.2.3浮点类型、格式和值 、5.1.13 值集转换、15.4 FP-strict Expressions 、对第 15 章后面其他部分的许多小更新)和 Java 虚拟机规范的类似部分(JVMS 2.3.2 浮点类型、值集和值*,第 2.8.2 节浮点模式 ,2.8.3值集)转换 ,以及对个别浮点指令的许多小更新)。值集和值集转换的概念将从 JLS 和 JVMS 中删除。JDK 中的实现更改将包括更新 HotSpot 虚拟机,使其永远不会在允许扩展指数值集的浮点模式下运行(这种模式必须存在于 strictfp
操作中)并更新 javac
以发出新的 lint 警告以防止不必要的使用的 strictfp
修饰符。
2.JEP 356:增强型伪随机数生成器
概括
为伪随机数生成器 (PRNG) 提供新的接口类型和实现,包括可跳转的 PRNG 和额外的一类可拆分 PRNG 算法 (LXM)。
目标
- 使在应用程序中交替使用各种 PRNG 算法变得更容易。
- 通过提供 PRNG 对象流更好地支持基于流的编程。
- 消除现有 PRNG 类中的代码重复。
- 小心地保留 class 的现有行为
java.util.Random
。
原因
- 使用遗留的 PRNG 类
Random
、ThreadLocalRandom
和SplittableRandom
,很难用其他算法替换应用程序中的任何一个,尽管它们都支持几乎相同的方法集。例如,如果一个应用程序使用 class 的实例Random
,它必然会声明 type 的变量Random
,它不能保存 class 的实例SplittableRandom
;更改要使用的应用程序SplittableRandom
需要更改用于保存 PRNG 对象的每个变量(包括方法参数)的类型。一个例外是它ThreadLocalRandom
是 的子类Random
,纯粹是为了允许 类型的变量Random
保存 的实例ThreadLocalRandom
,但ThreadLocalRandom
几乎覆盖了 的所有方法Random
。接口可以轻松解决这个问题。 - 传统类
Random
,ThreadLocalRandom
以及SplittableRandom
所有支持等方法,nextDouble()
和nextBoolean()
以及流产生方法,如ints()
和longs()
,但他们拥有完全独立的,几乎复制和粘贴的方式工作。重构此代码将使其更易于维护,此外,文档将使第三方更容易创建新的 PRNG 类,这些类也支持相同的完整方法套件。 - 2016 年,测试揭示了 class 使用的算法中的两个新弱点
SplittableRandom
。一方面,相对较小的修订可以避免这些弱点。另一方面,还发现了一类新的可拆分 PRNG 算法 (LXM),其速度几乎一样快,甚至更容易实现,并且似乎完全避免了容易出现的三类弱点SplittableRandom
。 - 能够从 PRNG 获取 PRNG 对象流使得使用流方法表达某些类型的代码变得更加容易。
- 文献中有许多 PRNG 算法不是可拆分的,而是可跳跃的(也许也是可跳跃的,即能够进行非常长的跳跃和普通跳跃),这一特性与拆分完全不同,但也有助于支持流PRNG 对象。过去,很难在 Java 中利用这一特性。可跳转 PRNG 算法的示例是 Xoshiro256** 和 Xoroshiro128+。
- Xoshiro256** 和 Xoroshiro128+:http ://xoshiro.di.unimi.it
具体描述
我们提供了一个新接口,RandomGenerator
为所有现有的和新的 PRNG 提供统一的 API。RandomGenerators
提供命名方法 ints
,longs
,doubles
,nextBoolean
,nextInt
,nextLong
, nextDouble
,和 nextFloat
,与他们所有的当前参数的变化。
我们提供了四个新的专门的 RandomGenerator 接口:
SplittableRandomGenerator
扩展RandomGenerator
并提供 名为split
and 的方法splits
。可拆分性允许用户从现有的 RandomGenerator 生成一个新的 RandomGenerator,这通常会产生统计上独立的结果。JumpableRandomGenerator
扩展RandomGenerator
并提供 名为jump
and 的方法jumps
。可跳跃性允许用户跳过中等数量的平局。LeapableRandomGenerator
扩展RandomGenerator
并提供 名为leap
and 的方法leaps
。Leapability 允许用户跳过大量的抽奖。ArbitrarilyJumpableRandomGenerator
扩展LeapableRandomGenerator
并且还提供了jump
和 的其他变体jumps
,允许指定任意跳跃距离。
我们提供了一个新类 RandomGeneratorFactory
,用于定位和构造 RandomGenerator
实现的实例。在 RandomGeneratorFactory
使用 ServiceLoader.Provider
注册API RandomGenerator
的实现。
我们重构了 Random
, ThreadLocalRandom
,SplittableRandom
以便共享它们的大部分实现代码,此外,还使这些代码也可以被其他算法重用。这个重构创建底层非公抽象类 AbstractRandomGenerator
,AbstractSplittableRandomGenerator
, AbstractJumpableRandomGenerator
,AbstractLeapableRandomGenerator
,和 AbstractArbitrarilyJumpableRandomGenerator
,为每个方法提供仅实现 nextInt()
,nextLong()
和(如果相关的话)任一 split()
,或 jump()
,或 jump()
和 leap()
,或 jump(distance)
。经过这次重构,Random
, ThreadLocalRandom
, 和 SplittableRandom
继承了 RandomGenerator
接口。注意,因为 SecureRandom
是 的子类 Random
,所有的实例 SecureRandom
也自动支持该 RandomGenerator
接口,无需重新编码 SecureRandom
类或其任何相关的实现引擎。
我们还添加了底层非公共类,这些类扩展 AbstractSplittableRandomGenerator
(并因此实现 SplittableRandomGenerator
和 RandomGenerator
)以支持 LXM 系列 PRNG 算法的六个特定成员:
L32X64MixRandom
L32X64StarStarRandom
L64X128MixRandom
L64X128StarStarRandom
L64X256MixRandom
L64X1024MixRandom
L128X128MixRandom
L128X256MixRandom
L128X1024MixRandom
LXM 算法的中心 nextLong(或 nextInt)方法的结构遵循 Sebastiano Vigna 在 2017 年 12 月提出的建议,即使用一个 LCG 子生成器和一个基于异或的子生成器(而不是两个 LCG 子生成器)将提供更长的周期、优越的等分布、可扩展性和更好的质量。这里的每个具体实现都结合了目前最著名的基于异或的生成器之一(xoroshiro 或 xoshiro,由 Blackman 和 Vigna 在"Scrambled Linear Pseudorandom Number Generators",ACM Trans. Math. Softw.,2021 中描述)与一个 LCG使用目前最著名的乘数之一(通过 Steele 和 Vigna 在 2019 年搜索更好的乘数找到),然后应用 Doug Lea 确定的混合函数。
我们还提供了这些广泛使用的 PRNG 算法的实现:
Xoshiro256PlusPlus
Xoroshiro128PlusPlus
上面提到的非公共抽象实现将来可能会作为随机数实现器 SPI 的一部分提供。
这套算法为 Java 程序员提供了空间、时间、质量和与其他语言兼容性之间的合理范围的权衡。
3.JEP 382:新的 macOS 渲染管线
概括
使用 Apple Metal API 为 macOS 实现 Java 2D 内部渲染管道,作为现有管道的替代方案,现有管道使用已弃用的 Apple OpenGL API。
目标
- 为使用 macOS Metal 框架的 Java 2D API 提供功能齐全的渲染管道。
- 如果 Apple 从未来版本的 macOS 中删除已弃用的 OpenGL API,请做好准备。
- 确保新管道到 Java 应用程序的透明度。
- 确保实现与现有 OpenGL 管道的功能奇偶校验。
- 在选定的实际应用程序和基准测试中提供与 OpenGL 管道一样好或更好的性能。
- 创建适合现有 Java 2D 管道模型的干净架构。
- 与 OpenGL 管道共存,直到它过时。
原因
两个主要因素促使在 macOS 上引入新的基于 Metal 的渲染管道:
- Apple于 2018 年 9 月在 macOS 10.14 中弃用了 OpenGL 渲染库。 macOS 上的Java 2D 完全依赖 OpenGL 进行其内部渲染管道,因此需要新的管道实现。
- Apple 声称Metal 框架(它们替代 OpenGL)具有卓越的性能。对于 Java 2D API,通常是这种情况,但有一些例外。
具体描述
大多数图形 Java 应用程序是使用 Swing UI 工具包编写的,该工具包通过 Java 2D API 呈现。在内部,Java 2D 可以使用软件渲染和屏幕上的 blit,也可以使用特定于平台的 API,例如 Linux 上的 X11/Xrender、Windows 上的 Direct3D 或 macOS 上的 OpenGL。这些特定于平台的 API 通常提供比软件渲染更好的性能,并且通常会减轻 CPU 的负担。Metal 是用于此类渲染的新 macOS 平台 API,取代了已弃用的 OpenGL API。(该名称与 Swing "金属"外观和感觉无关;这只是巧合。)
我们创建了大量新的内部实现代码来使用 Metal 框架,就像我们已经为其他特定于平台的 API 所做的那样。虽然很容易适应现有框架,但新代码在使用图形硬件方面更加现代,使用着色器而不是固定功能管道。这些更改仅限于特定于 macOS 的代码,甚至只更新了 Metal 和 OpenGL 之间共享的最少量代码。我们没有引入任何新的 Java API,也没有改变任何现有的 API。
Metal 管道可以与 OpenGL 管道共存。当图形应用程序启动时,会选择其中一个。目前,OpenGL 仍然是默认设置。仅当在启动时指定或 OpenGL 初始化失败时才使用 Metal,就像在没有 OpenGL 支持的未来版本的 macOS 中一样。
在集成此 JEP 时,Apple 尚未删除 OpenGL。在此之前,应用程序可以通过 -Dsun.java2d.metal=true
在 java
命令行上指定来选择加入 Metal 。我们将在未来的版本中将 Metal 渲染管线设为默认。
在集成到 JDK 之前,我们在Project Lanai 中对这个 JEP 进行了工作。
4.JEP 391:macOS/AArch64 端口
概括
将 JDK 移植到 macOS/AArch64。
原因
Apple 宣布了一项将其 Macintosh 计算机系列从 x64 过渡到 AArch64的长期计划。因此,我们希望看到对 JDK 的 macOS/AArch64 端口的广泛需求。
尽管可以通过 macOS 的内置Rosetta 2 转换器在基于 AArch64 的系统上运行 JDK 的 macOS/x64 版本,但该翻译几乎肯定会带来显着的性能损失。
具体描述
Linux 的 AArch64 端口(JEP 237)已经存在,Windows 的 AArch64 端口(JEP 388)的工作正在进行中。我们希望通过使用条件编译(在 JDK 的端口中很常见)来重用来自这些端口的现有 AArch64 代码,以适应低级约定的差异,例如应用程序二进制接口 (ABI) 和保留的处理器寄存器集。
macOS/AArch64 禁止内存段同时可执行和可写,这一策略称为write-xor-execute (W^X)。HotSpot VM 会定期创建和修改可执行代码,因此此 JEP 将在 HotSpot 中为 macOS/AArch64 实现 W^X 支持。
5.JEP 398:弃用 Applet API 以进行删除
概述
弃用 Applet API 以进行删除。它基本上无关紧要,因为所有 Web 浏览器供应商都已取消对 Java 浏览器插件的支持或宣布了这样做的计划。 Java 9 中的JEP 289先前已弃用 Applet API,但并未将其删除。
具体内容
弃用或移除标准 Java API 的这些类和接口:
java.applet.Applet
java.applet.AppletStub
java.applet.AppletContext
java.applet.AudioClip
javax.swing.JApplet
java.beans.AppletInitializer
弃用(删除)引用上述类和接口的任何 API 元素,包括以下中的方法和字段:
java.beans.Beans
javax.swing.RepaintManager
javax.naming.Context
6.JEP 403:强封装 JDK 内部
概述
强烈封装 JDK 的所有内部元素,除了 关键的内部 API,如 sun.misc.Unsafe
. 不再可能通过单个命令行选项来放松内部元素的强封装,就像在 JDK 9 到 JDK 16 中那样。
这个 JEP 是JEP 396的继承者,它将 JDK 从默认的宽松强封装 转换为默认 强封装,同时允许用户根据需要返回到轻松的姿势。本 JEP 的目标、非目标、动机、风险和假设部分与 JEP 396 的部分基本相同,但为了读者的方便在此处复制。
目标
- 继续提高 JDK 的安全性和可维护性,这是Project Jigsaw的主要目标之一。
- 鼓励开发人员从使用内部元素迁移到使用标准 API,以便他们和他们的用户可以轻松升级到未来的 Java 版本。
原因
多年来,各种库、框架、工具和应用程序的开发人员以损害安全性和可维护性的方式使用 JDK 的内部元素。特别是:
- 包的一些非
public
类、方法和字段java.*
定义了特权操作,例如在特定类加载器中定义新类的能力,而其他则传送敏感数据,例如加密密钥。尽管在java.*
包中,但这些元素是 JDK 的内部元素。外部代码通过反射使用这些内部元素会使平台的安全性处于危险之中。 - 包的所有类、方法和字段
sun.*
都是 JDK 的内部 API。大多数类,方法和领域com.sun.*
,jdk.*
以及org.*
包也是内部API。这些 API 从来都不是标准的,从来没有得到支持,也从来没有打算供外部使用。外部代码使用这些内部元素是一种持续的维护负担。为了不破坏现有代码,保留这些 API 所花费的时间和精力可以更好地用于推动平台向前发展。
在 Java 9 中,我们通过利用模块来限制对其内部元素的访问,提高了 JDK 的安全性和可维护性。模块提供强封装,这意味着
- 模块外的代码只能访问该 模块导出的包的
public
和protected
元素,并且 protected
此外,元素只能从定义它们的类的子类中访问。
强封装适用于编译时和运行时,包括编译代码尝试在运行时通过反射访问元素时。public
导出包的非元素和未导出包的所有元素都被称为强封装。
在 JDK 9 及更高版本中,我们强烈封装了所有新的内部元素,从而限制了对它们的访问。然而,为了帮助迁移,我们故意选择不在运行时强封装 JDK 8 中已经存在的内部元素。因此类路径上的库和应用程序代码可以继续使用反射来访问非 public
元素的 java.*
包,以及所有元素 sun.*
和其他内部包,用于在JDK 8存在这种安排被称为包宽松强大的封装性,并且在JDK 9的默认行为。
我们早在 2017 年 9 月就发布了 JDK 9。 JDK 的大多数常用内部元素现在都有标准替代品。开发人员必须在三年中,从标准的API,如JDK的内部元素迁移走 java.lang.invoke.MethodHandles.Lookup::defineClass
,java.util.Base64
和 java.lang.ref.Cleaner
。许多库、框架和工具维护者已经完成了迁移并发布了其组件的更新版本。现在对宽松强封装的需求比 2017 年弱,而且逐年进一步减弱。
在 2021 年 3 月发布的 JDK 16 中,我们迈出了下一步,以强封装 JDK 的所有内部元素。 JEP 396将强封装作为默认行为,但关键的内部 API(如 sun.misc.Unsafe
)仍然可用。在 JDK 16 中,最终用户仍然可以选择宽松的强封装,以便访问 JDK 8 中存在的内部元素。
我们现在准备通过移除选择宽松强封装的能力,在这个旅程中再迈出一步。这意味着 JDK 的所有内部元素都将被强封装,除了关键的内部 API,如 sun.misc.Unsafe
.
具体描述
松弛强封装由启动器选项控制 --illegal-access
。这个选项由JEP 261引入,被挑衅地命名以阻止其使用。在 JDK 16 及更早版本中,它的工作方式如下:
-
--illegal-access=permit
安排 JDK 8 中存在的每个包都对未命名模块中的代码开放。因此,类路径上的代码可以继续使用反射来访问包的非公共元素java.*
,以及sun.*
JDK 8 中存在的包和其他内部包的所有元素。对任何此类元素的第一次反射访问操作会导致发出警告,但在那之后不会发出警告。此模式是 JDK 9 到 JDK 15 的默认模式。
-
--illegal-access=warn``permit
除了针对每个非法反射访问操作发出警告消息之外,其他都相同。 -
--illegal-access=debug``warn
除了为每个非法反射访问操作发出警告消息和堆栈跟踪之外,其他都相同。 -
--illegal-access=deny
禁用所有非法访问操作,但由其他命令行选项启用的操作除外,例如 ,--add-opens
。此模式是 JDK 16 中的默认模式。
作为强封装 JDK 的所有内部元素的下一步,我们建议使该 --illegal-access
选项过时。此选项的任何使用,无论是使用 permit
、warn
、debug
或 deny
,都不会产生任何影响,只会发出警告消息。我们希望 --illegal-access
在未来的版本中完全删除该选项。
通过此更改,最终用户将无法再使用该 --illegal-access
选项来启用对 JDK 内部元素的访问。(影响到包的列表,请点击这里。) **的 sun.misc
和 sun.reflect
软件包将仍然由出口 jdk.unsupported
模块,并且仍然是开放的,这样的代码可以通过反射访问他们的非公开内容。**不会以这种方式打开其他 JDK 包。
仍然可以使用--add-opens
命令行选项或Add-Opens
JAR 文件清单属性来打开特定的包。
7.JEP 407:删除 RMI 激活
概括
删除远程方法调用 (RMI) 激活机制,同时保留 RMI 的其余部分。
原因
RMI 激活机制已过时且已废弃。它已被Java SE 15 中的JEP 385弃用。没有收到针对该弃用的评论。请参阅JEP 385了解完整的背景、原理、风险和替代方案。
Java EE 平台包含一项称为JavaBeans Activation Framework (JAF) 的技术。作为Eclipse EE4J计划的一部分,它后来更名为Jakarta Activation。JavaBeans Activation 和 Jakarta Activation 技术与 RMI Activation 完全无关,它们不受从 Java SE 中删除 RMI Activation 的影响。
具体描述
java.rmi.activation
从 Java SE API 规范中删除包- 更新RMI 规范以删除提及 RMI 激活
- 去掉实现RMI激活机制的JDK库代码
- 删除 RMI 激活机制的 JDK 回归测试
- 删除 JDK 的
rmid
激活守护进程及其文档
8.JEP 410:删除实验性 AOT 和 JIT 编译器
概括
删除实验性的基于 Java 的提前 (AOT) 和即时 (JIT) 编译器。该编译器自推出以来几乎没有什么用处,维护它所需的工作量很大。保留实验性的 Java 级 JVM 编译器接口 (JVMCI),以便开发人员可以继续使用外部构建的编译器版本进行 JIT 编译。
原因
提前编译(该 jaotc
工具)已通过JEP 295作为实验性功能合并到 JDK 9 中。该 jaotc
工具使用 Graal 编译器,它本身是用 Java 编写的,用于 AOT 编译。
Graal 编译器通过JEP 317在 JDK 10 中作为实验性 JIT 编译器提供。
自从引入这些实验性功能以来,我们几乎没有看到它们的使用,并且维护和增强它们所需的工作量很大。这些特性没有包含在 Oracle 发布的 JDK 16 版本中,并且没有人抱怨。
具体操作
删除三个 JDK 模块:
jdk.aot
---jaotc
工具jdk.internal.vm.compiler
--- Graal 编译器jdk.internal.vm.compiler.management
--- 格拉尔的MBean
保留这两个 Graal 相关的源文件,以便 JVMCI 模块 ( jdk.internal.vm.ci
, JEP 243 ) 继续构建:
src/jdk.internal.vm.compiler/share/classes/module-info.java
src/jdk.internal.vm.compiler.management/share/classes/module-info.java
删除与AOT编译相关的HotSpot代码:
src/hotspot/share/aot
--- 转储和加载 AOT 代码- 由保护的附加代码
#if INCLUDE_AOT
最后,删除测试以及与 Graal 和 AOT 编译相关的 makefile 中的代码。
9.JEP 411:弃用安全管理器以进行删除
概述
弃用安全管理器以在未来版本中移除。安全管理器可追溯到 Java 1.0。多年来,它一直不是保护客户端 Java 代码的主要手段,也很少用于保护服务器端代码。为了推动 Java 向前发展,我们打算弃用安全管理器,以便与旧 Applet API ( JEP 398 )一起删除。
目标
- 为开发人员在 Java 的未来版本中移除安全管理器做好准备。
- 警告用户他们的 Java 应用程序是否依赖于安全管理器。
- 评估是否需要新的 API 或机制来解决使用安全管理器的特定狭窄用例,例如阻塞
System::exit
。
原因
Java 平台强调安全性。数据的完整性 受到 Java 语言和 VM 内置内存安全的保护:变量在使用前被初始化,数组边界被检查,内存释放是完全自动的。同时,数据的机密性受到 Java 类库对现代加密算法和协议(如 SHA-3、EdDSA 和 TLS 1.3)的可信实现的保护。安全是一门动态的科学,因此我们不断更新 Java 平台以解决新的漏洞并反映新的行业态势,例如通过弃用弱加密协议。
一个长期存在的安全元素是安全管理器,它可以追溯到 Java 1.0。在 Web 浏览器下载 Java 小程序的时代,安全管理器通过在沙箱中 运行小程序来保护用户机器的完整性及其数据的机密性,从而拒绝访问文件系统或网络等资源。Java 类库的小尺寸 java.*
------Java 1.0 中只有八个包------使得代码变得可行,例如,java.io
在执行任何操作之前咨询安全管理器。安全管理器在不受信任的代码 (来自远程机器的小程序)和受信任的代码 之间划清了界限(本地机器上的类):它会批准所有涉及可信代码资源访问的操作,但拒绝不可信代码的资源访问。
随着对 Java 兴趣的增长,我们引入了签名小程序 以允许安全管理器信任远程代码,从而允许小程序访问与通过 java
命令行运行的本地代码相同的资源。同时,Java 类库也在迅速扩展------Java 1.1 引入了 JavaBeans、JDBC、反射、RMI 和序列化------这意味着受信任的代码可以访问重要的新资源,如数据库连接、RMI 服务器和反射对象。允许所有受信任的代码访问所有资源是不可取的,因此在 Java 1.2 中,我们重新设计了安全管理器以专注于应用*最小权限原则:*默认情况下,所有代码都将被视为不受信任,受阻止访问资源的沙盒式控制的约束,并且用户将通过授予他们访问特定资源的特定权限来信任特定的代码库。理论上,类路径上的应用程序 JAR 在使用 JDK 的方式方面可能比来自 Internet 的小程序更受限制。限制权限被视为限制代码体中可能存在的任何漏洞影响的一种方式------实际上是一种纵深防御机制。
因此,安全经理希望防范两种威胁:恶意意图 (尤其是远程代码)和意外漏洞(尤其是本地代码)。
由于 Java 平台不再支持小程序,远程代码的恶意威胁已经消退。Applet API在 2017 年在 Java 9 中被弃用,然后在 2021 年在 Java 17 中被弃用,并打算在未来的版本中将其删除。2018 年,运行小程序的闭源浏览器插件与闭源 Java Web Start 技术一起从 Oracle 的 JDK 11 中删除。因此,安全管理器防范的许多风险不再重要。此外,安全管理器无法防范许多现在很重要的风险。安全经理无法解决行业领导者在 2020 年确定的25 个最危险问题中的19 个,因此诸如 XML 外部实体引用 (XXE) 注入和不正确的输入验证等问题需要 Java 类库中的直接对策。(例如,JAXP 可以防止 XXE 攻击和 XML 实体扩展,而序列化过滤可以防止恶意数据在造成任何损害之前被反序列化。)安全管理器也无法防止基于推测执行漏洞的恶意行为。
不幸的是,安全管理器对恶意意图缺乏效力,因为安全管理器必须被编入 Java 类库的结构中。因此,这是一个持续的维护负担。必须评估所有新功能和 API,以确保它们在启用安全管理器时正确运行。基于最小特权原则的访问控制可以在Java 1.0中的类库已经可行,但快速增长 java.*
和 javax.*
包导致了整个 JDK 中的数十个权限和数百个权限检查。这是保持安全的一个重要表面区域,特别是因为权限可以以令人惊讶的方式进行交互。某些权限,例如,允许应用程序或库代码执行一系列安全操作,其整体效果足够不安全,如果直接授予则需要更强大的权限。
使用安全管理器几乎不可能解决本地代码中意外漏洞的威胁。许多声称安全管理器被广泛用于保护本地代码的说法经不起推敲。它在生产中的使用远比许多人想象的要少。它没有使用的原因有很多:
- 脆弱的权限模型 --- 希望从安全管理器中受益的应用程序开发人员必须仔细授予应用程序执行所有操作所需的所有权限。没有办法获得部分安全性,其中只有少数资源受到访问控制。例如,假设开发人员担心非法访问数据,因此希望授予仅从特定目录读取文件的权限。授予文件读取权限是不够的,因为应用程序几乎肯定会使用 Java 类库中除了读取文件(例如,写入文件)之外的其他操作,而这些其他操作将被安全管理器拒绝,因为代码将没有适当的许可。只有仔细记录他们的代码如何与 Java 类库中的安全敏感操作交互的开发人员才能授予必要的权限。这不是常见的开发人员工作流程。(安全管理员不允许否定权限,可以表示"授予除读取文件以外的所有操作的权限"。)
- 困难的编程模型 ------安全经理通过检查导致操作的所有运行代码的权限来批准安全敏感操作。这使得编写与安全管理器一起运行的库变得困难,因为库开发人员记录其库代码所需的权限是不够的。除了已授予该代码的任何权限之外,使用该库的应用程序开发人员还需要为其应用程序代码授予相同的权限。这违反了最小特权原则,因为应用程序代码可能不需要库的权限来进行自己的操作。库开发人员可以通过谨慎使用
java.security.AccessController
API 要求安全管理器只考虑库的权限,但是这个和其他安全编码指南的复杂性远远超出了大多数开发人员的兴趣。应用程序开发人员的阻力最小的路径通常是授予AllPermission
任何相关的 JAR 文件,但这又与最小权限原则背道而驰。 - 性能不佳------安全管理器的核心是一个复杂的访问控制算法,它通常会带来不可接受的性能损失。因此,默认情况下,对于在命令行上运行的 JVM,安全管理器始终处于禁用状态。这进一步降低了开发人员对投资使库和应用程序与安全管理器一起运行的兴趣。缺乏帮助推断和验证权限的工具是另一个障碍。
在引入安全管理器的四分之一个世纪以来,采用率一直很低。只有少数应用程序附带限制其自身操作的策略文件(例如,ElasticSearch)。类似地,只有少数框架附带策略文件(例如Tomcat),使用这些框架构建应用程序的开发人员仍然面临着确定他们自己的代码和他们使用的库所需的权限这一几乎无法克服的挑战。一些框架(例如NetBeans)避开策略文件而是实现自定义安全管理器以防止插件调用 System::exit
或者深入了解代码的行为,例如它是否打开文件和网络连接------我们认为通过其他方式更好地服务的用例。
总之,使用安全管理器开发现代 Java 应用程序没有太大的兴趣。根据权限做出访问控制决策既笨拙又缓慢,并且在整个行业中失宠;例如,.NET不再支持它。通过在 Java 平台的较低级别提供完整性可以更好地实现安全性------例如,通过加强模块边界 ( JEP 403 ) 以防止访问 JDK 实现细节,并强化实现本身--- 并通过容器和管理程序等进程外机制将整个 Java 运行时与敏感资源隔离。为了推动 Java 平台向前发展,我们将弃用从 JDK 中删除的旧安全管理器技术。我们计划在多个版本中弃用和削弱安全管理器的功能,同时为诸如阻塞等任务 System::exit
和其他被认为足够重要以进行替换的用例创建替代 API 。
具体操作
在 Java 17 中,我们将:
- 弃用(删除)大多数与安全管理器相关的类和方法。
- 如果在命令行上启用了安全管理器,则在启动时发出警告消息。
- 如果 Java 应用程序或库动态安装安全管理器,则在运行时发出警告消息。
在 Java 18 中,除非最终用户明确选择允许,否则我们将阻止 Java 应用程序或库动态安装安全管理器。从历史上看,Java 应用程序或库总是被允许动态安装安全管理器,但从Java 12 开始,最终用户已经能够通过在命令行 ( )上设置系统属性 java.security.manager
来阻止它------这会导致抛出. 在Java中18起,默认值将是,如果不通过其他方式设置。因此,调用的应用程序和库可能会由于意外的. 为了像以前一样工作,最终用户必须设置 disallow``java -Djava.security.manager=disallow ...``System::setSecurityManager``UnsupportedOperationException``java.security.manager``disallow``java -D...``System::setSecurityManager``UnsupportedOperationException``System::setSecurityManager``java.security.manager
到 allow
命令行 ( java -Djava.security.manager=allow ...
)。
在 Java 18 及更高版本中,我们将降级其他安全管理器 API,以便它们保持原样,但功能有限或没有功能。例如,我们可以 AccessController::doPrivileged
简单地修改来运行给定的动作,或者修改 System::getSecurityManager
总是返回 null
。这将允许支持安全管理器并针对以前的 Java 版本编译的库继续工作而无需更改甚至重新编译。一旦这样做的兼容性风险下降到可接受的水平,我们希望删除这些 API。
在 Java 18 及更高版本中,我们可能会更改 Java SE API 定义,以便之前执行权限检查的操作不再执行它们,或者在启用安全管理器时执行更少的检查。因此,@throws SecurityException
将出现在 API 规范中较少的方法中。
10.JEP 412:外部函数和内存 API(孵化器)
概括
介绍一个 API,Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过有效调用外部函数(即 JVM 之外的代码),以及安全地访问外部内存(即不由 JVM 管理的内存),API 使 Java 程序能够调用本地库和处理本地数据,而没有JNI。
历史
本 JEP 中提出的 API 是两个孵化 API 的演变:外部内存访问 API 和外部链接器 API。Foreign-Memory Access API 最初由JEP 370提出,并于 2019 年末针对 Java 14 作为孵化 API;它由Java 15 中的JEP 383和Java 16 中的JEP 393重新孵化。外部链接器 API 最初由JEP 389提出,并于 2020 年末针对 Java 16,也作为孵化 API。
目标
- 易用性 --- 用高级的纯 Java 开发模型替换 Java 本机接口 ( JNI )。
- 性能 --- 提供与现有 API(例如 JNI 和
sun.misc.Unsafe
. - 通用性--- 提供对不同类型的外部内存(例如,本机内存、持久内存和托管堆内存)进行操作的方法,并随着时间的推移适应其他平台(例如,32 位 x86)和用其他语言编写的外部函数比 C(例如,C++、Fortran)。
- 安全------默认禁用不安全的操作,只有在应用程序开发人员或最终用户明确选择后才允许它们。
原因
Java 平台一直为希望超越 JVM 并与其他平台交互的库和应用程序开发人员提供丰富的基础。Java API 以方便可靠的方式公开非 Java 资源,无论是访问远程数据 (JDBC)、调用 Web 服务(HTTP 客户端)、服务远程客户端(NIO 通道)还是与本地进程通信(Unix 域套接字) . 不幸的是,Java 开发人员在访问一种重要的非 Java 资源时仍然面临重大障碍:与 JVM 位于同一台机器上但在 Java 运行时之外的代码和数据。
外部存储
存储在 Java 运行时之外的内存中的数据称为堆外 数据。(堆 是 Java 对象所在的地方------堆上 数据------以及垃圾收集器工作的地方。)访问堆外数据对于Tensorflow、Ignite、Lucene和Netty等流行 Java 库的性能至关重要,主要是因为它让他们避免了与垃圾收集相关的成本和不可预测性。它还允许通过将文件映射到内存中来序列化和反序列化数据结构,例如mmap
。然而,Java 平台目前还没有为访问堆外数据提供令人满意的解决方案。
- 该
ByteBuffer
API允许创建的直接 被分配离堆字节缓冲区,但他们的最大大小为2 GB的,他们得不到及时释放。这些和其他限制源于这样一个事实,即ByteBuffer
API 不仅设计用于堆外内存访问,而且还用于生产者/消费者在字符集编码/解码和部分 I/O 操作等领域交换批量数据。在这种情况下,多年来提交的许多堆外增强请求(例如4496703、6558368、4837564和5029431)一直无法满足。 - 该
sun.misc.Unsafe
API对堆数据自曝存储器存取操作也为离堆数据的工作。使用Unsafe
是高效的,因为它的内存访问操作被定义为 HotSpot JVM 内部函数并由 JIT 编译器优化。但是,使用Unsafe
是危险的,因为它允许访问任何内存位置。这意味着 Java 程序可以通过访问一个已经释放的位置来使 JVM 崩溃;由于这个原因和其他原因,Unsafe
一直强烈反对使用。 - 使用 JNI 调用本地库然后访问堆外数据是可能的,但性能开销很少使它适用:从 Java 到本地比访问内存慢几个数量级,因为 JNI 方法调用没有从许多常见的JIT 优化,例如内联。
综上所述,在访问堆外数据时,Java 开发人员面临一个两难选择:他们应该选择安全但效率低下的路径 ( ByteBuffer
) 还是应该放弃安全以支持性能 ( Unsafe
)?他们需要的是用于访问堆外数据(即外部内存)的受支持 API,从头开始设计以确保安全并考虑到 JIT 优化。
外部函数
JNI 从 Java 1.1 开始就支持调用本机代码(即外部函数),但是由于多种原因它不够用。
- JNI 涉及几个乏味的工件:Java API(
native
方法)、从 Java API 派生的 C 头文件,以及调用感兴趣的本机库的 C 实现。Java 开发人员必须跨多个工具链工作以保持依赖于平台的工件同步,这在本机库快速发展时尤其繁重。 - JNI 只能与用语言(通常是 C 和 C++)编写的库进行互操作,这些语言使用操作系统和 CPU 的调用约定,JVM 是为其构建的。一个
native
方法不能被用来调用写在使用不同的约定语言的功能。 - JNI 不协调 Java 类型系统与 C 类型系统。Java 中的聚合数据是用对象表示的,而 C 中的聚合数据是用结构体表示的,因此任何传递给
native
方法的Java 对象都必须由本机代码费力地解包。例如,考虑Person
Java 中的记录类:将Person
对象传递给native
方法将需要本机代码使用 JNI 的 C API从对象中提取字段(例如,firstName
和lastName
)。因此,Java 开发人员有时会将他们的数据扁平化为单个对象(例如,字节数组或直接字节缓冲区),但更多时候,由于通过 JNI 传递 Java 对象很慢,他们使用Unsafe
API 来分配堆外内存和将其地址作为native
方法传递给方法long
--- 这使得 Java 代码非常不安全!
多年来,出现了许多框架来填补 JNI 留下的空白,包括JNA、JNR和JavaCPP。尽管这些框架通常比 JNI 有了显着的改进,但情况仍然不太理想,尤其是与提供一流的本地互操作性的语言相比时。例如,Python 的ctypes包可以在本地库中动态包装函数,无需任何胶水代码。其他语言,例如Rust,提供了从 C/C++ 头文件机械地派生本机包装器的工具。
最终,Java 开发人员应该有一个受支持的 API,让他们可以直接使用任何被认为对特定任务有用的本机库,而不需要 JNI 的乏味粘连和笨拙。一个极好的构建方法 是handles,它是在 Java 7 中引入的,用于支持 JVM 上的快速动态语言。通过方法句柄公开本机代码将从根本上简化编写、构建和分发依赖本机库的 Java 库的任务。此外,能够对外部函数(即本机代码)和外部内存(即堆外数据)进行建模的 API 将为第三方本机互操作框架提供坚实的基础。
描述
外部函数和内存 API (FFM API) 定义了类和接口,以便库和应用程序中的客户端代码可以
- 分配外部内存 (
MemorySegment
,MemoryAddress
, 和SegmentAllocator
), - 操作和访问结构化的外部内存 (
MemoryLayout
,MemoryHandles
, 和MemoryAccess
), - 管理外部资源的生命周期 (
ResourceScope
),以及 - 调用外部函数(
SymbolLookup
和CLinker
)。
FFM API 位于模块的 jdk.incubator.foreign
包中 jdk.incubator.foreign
。
例子
作为使用 FFM API 的简短示例,这里是 Java 代码,它获取 C 库函数的方法句柄 radixsort
,然后使用它对 Java 数组中的四个字符串进行排序(省略了一些细节):
java
// 1. Find foreign function on the C library path
MethodHandle radixSort = CLinker.getInstance().downcallHandle(
CLinker.systemLookup().lookup("radixsort"), ...);
// 2. Allocate on-heap memory to store four strings
String[] javaStrings = { "mouse", "cat", "dog", "car" };
// 3. Allocate off-heap memory to store four pointers
MemorySegment offHeap = MemorySegment.allocateNative(
MemoryLayout.ofSequence(javaStrings.length,
CLinker.C_POINTER), ...);
// 4. Copy the strings from on-heap to off-heap
for (int i = 0; i < javaStrings.length; i++) {
// Allocate a string off-heap, then store a pointer to it
MemorySegment cString = CLinker.toCString(javaStrings[i], newImplicitScope());
MemoryAccess.setAddressAtIndex(offHeap, i, cString.address());
}
// 5. Sort the off-heap data by calling the foreign function
radixSort.invoke(offHeap.address(), javaStrings.length, MemoryAddress.NULL, '\0');
// 6. Copy the (reordered) strings from off-heap to on-heap
for (int i = 0; i < javaStrings.length; i++) {
MemoryAddress cStringPtr = MemoryAccess.getAddressAtIndex(offHeap, i);
javaStrings[i] = CLinker.toJavaStringRestricted(cStringPtr);
}
assert Arrays.equals(javaStrings, new String[] {"car", "cat", "dog", "mouse"}); // true
这段代码比任何使用 JNI 的解决方案都清晰得多,因为隐藏在 native
方法调用后面的隐式转换和内存取消引用现在直接用 Java 表示。也可以使用现代 Java 习语;例如,流可以允许多个线程在堆上和堆外内存之间并行复制数据。