JDK 14 即将发布,你所要知道的都在这里!
Categories: 编程
[toc]
公众号
coding 笔记、点滴记录,以后的文章也会同步到公众号(Coding Insight)中,大家关注 ^_^
我的博客地址:博客主页。

JDK 14 预计在 2020 年 3 月 17 日发布,详情见 JDK 14 官方计划。
官方计划
- 2019/12/12 Rampdown Phase One (fork from main line)
- 2020/01/16 Rampdown Phase Two
- 2020/02/06 Initial Release Candidate
- 2020/02/20 Final Release Candidate
- 2020/03/17 General Availability
特性预览
- 305: instanceof 的模式匹配 (预览)
- 343: 打包工具 (Incubator)
- 345: G1 的 NUMA 内存分配优化
- 349: JFR 事件流
- 352: 非原子性的字节缓冲区映射
- 358: 友好的空指针异常
- 359: Records (预览)
- 361: Switch 表达式 (标准)
- 362: 弃用 Solaris 和 SPARC 端口
- 363: 移除 CMS(Concurrent Mark Sweep)垃圾收集器
- 364: macOS 系统上的 ZGC
- 365: Windows 系统上的 ZGC
- 366: 弃用 ParallelScavenge + SerialOld GC 组合
- 367: 移除 Pack200 Tools 和 API
- 368: 文本块 (第二个预览版)
- 370: 外部存储器 API (Incubator)
深入理解新特性
305: instanceof 的模式匹配 (预览)
JEP 305: Pattern Matching for instanceof (Preview)
使 instanceof 运算符具有模式匹配的能力。模式匹配能够使程序的通用逻辑更加简洁,同时也更加安全。
动机
几乎每个程序都包含判断表达式是否具有某种类型的逻辑,程序会对不同类型进行不同的处理。所有 Java 程序员都熟悉下面的 instanceof-and-cast 用法:
if (obj instanceof String) {
String s = (String) obj;
// use s
}
这段程序做了 3 件事:
- 一个判断(obj 是一个 String?)
- 一个类型转换(将对象 obj 强制类型转换为 String)
- 声明一个新的本地变量 s,指向上面的 obj String
这种模式很简单,并且几乎所有 Java 程序员都可以理解。但是出于以下原因,上述做法并不是最理想的:
- 语法乏味
- 同时执行类型检测和类型转换并不是必要的
- String 类型在程序中出现了 3 次,这混淆了后面更重要的逻辑
- 重复的代码容易滋生错误
与其把上述方式作为临时解决方案,不如去使用模式匹配。我们可以通过模式匹配简洁地表达对象,并允许各种语句和表达式对其进行测试。许多语言——从 Haskell 到 C#,都因为简洁性和安全性选择了模式匹配。
具体描述
示例代码:
if (obj instanceof String s) {
// can use s here
} else {
// can't use s here
}
instanceof 运算符 “匹配” 规则如下:
- 如果 obj 是 String 类型,则将 obj 类型转换为 String,并将其赋值给变量
s。绑定的变量作用域为 if 语句内部,并不在 false 语句块内。
343: 打包工具 (Incubator)
JEP 343: Packaging Tool (Incubator)
创建用于打包能独立运行的 Java 应用程序的工具。
动机
许多 Java 程序需要以 “first-class” 方式安装在本机平台,而不是简单地将其放置在类路径或模块路径上。对于应用开发人员来说,仅仅是交付简单的 JAR 文件是不够的。他们必须提供适合本机平台的软件安装包:需要以用户熟悉的方式分发、安装和卸载 Java 程序。比如在 Windows 上,用户希望双击一个软件包就能安装,然后可以在控制面板上卸下该软件;在 MacOS 上,用户希望双击 DMG 文件,将其应用程序拖放到 Application 文件夹中。
描述
jpackage 打包工具可以将 Java 应用程序打包为针对特定平台的安装包,这个安装包包含所有必需的依赖项。该应用程序可以以普通 JAR 文件集合或模块集合的方式提供。软件包格式可以分为:
- Linux:deb 和 rpm
- macOS:pkg 和 dmg
- Windows:msi 和 exe
基本用法:非模块化应用
假设你有一个由 JAR 文件组成的应用程序,所有应用程序都位于 lib 目录下,并且主类在 lib/main.jar 中。下列命令
$ jpackage --name myapp --input lib --main-jar main.jar
将以本地系统的默认格式打包应用程序,并将生成的打包文件保留在当前目录中。如果 main.jar 中的 MANIFEST.MF 文件没有 Main-Class 属性,我们必须显式地指定主类:
$ jpackage --name myapp --input lib --main-jar main.jar --main-class myapp.Main
打包的名称是 myapp。要启动该应用程序,启动器将从输入目录复制的每个 JAR 文件都放在 JVM 的类路径上。
如果希望生成默认格式以外的软件安装包,可以使用 --type 选项。例如要在 macOS 上生成 pkg 文件(而不是 dmg 文件),我们可以使用下面的命令:
$ jpackage --name myapp --input lib --main-jar main.jar --type pkg
基本用法:模块化应用
如果你有一个模块化应用程序,该应用程序由 lib 目录中的模块化 JAR 文件和 / 或 JMOD 文件组成,并且主类位于 myapp 模块中,则下面的命令
$ jpackage --name myapp --module-path lib -m myapp
能够将其打包。如果 myapp 模块无法识别主类,则必须明确指定:
$ jpackage --name myapp --module-path lib -m myapp/myapp.Main
G1 的 NUMA 内存分配优化
JEP 345: NUMA-Aware Memory Allocation for G1
通过实现可识别 NUMA 的内存分配,提高大型计算机上的 G1 性能。
动机
现代的多插槽计算机越来越多地具有非统一的内存访问(NUMA,non-uniform memory access),即内存与每个插槽或内核之间的距离并不相等。 套接字之间的内存访问具有不同的性能特征,访问距离更远的套接字通常具有更多的延迟。
通过 -XX:+UseParallelGC 启用的并行垃圾收集器,已经 NUMA-aware 很长时间了,这提高了运行在单个 JVM 里多个套接字的性能。其他 HotSpot 收集器没有利用此功能,这意味着他们无法利用这种垂直多路 NUMA 缩放功能。大型企业应用程序尤其倾向于在多个套接字上以大型堆配置运行,但是他们希望在单个 JVM 中运行具有可管理性优势。 使用 G1 收集器的用户越来越多地遇到这种扩展瓶颈。
描述
G1 的堆内存是固定大小的 region 的集合。一个 region 通常是一组物理页面,尽管在使用大页面(通过 - XX:+UseLargePages 设定)时,一个物理页面由多个 region 组成。
如果指定了 +XX:+UseNUMA 选项,JVM 在初始化时,会将 region 平均分配在可用的 NUMA 节点上。
在开始时就为每个 NUM 节点分配固定的 region 有点不灵活,但是可以通过以下机制来缓解:为了给 mutator 线程分配新的对象,G1 可能需要一个新的 region。JVM 会从 NUMA 节点中优先选择与当前线程绑定的空闲区域来执行此操作,这样新的对象就可以在同一个 NUMA 的新生代中。如果同一 NUMA 节点上没有空闲的 region,G1 将触发垃圾回收操作。另一种方法是,从距离最近的 NUMA 节点开始,按照距离顺序在其他 NUMA 节点中搜索可用 region。
在老年代中,不会尝试将对象保存在同一个 NUMA 节点中。humongous region 也并不适用这项分配策略。
349: JFR 事件流
暴露 JDK Flight Recorder 数据,方便使用者持续监控。
动机
HotSpot VM 通过 JFR 产生的数据点超过 500 个,但是使用者只能通过解析日志文件的方法使用它们。
用户要想消费这些数据,必须开始一个记录并停止,将内容转储到磁盘上,然后解析记录文件。这对于应用程序分析非常有效,但是监控数据却十分不方便(例如显示动态更新数据的仪表盘)。
与创建记录相关的开销包括:
- 发出在创建新记录时必须发生的事件
- 写入事件元数据(例如字段布局)
- 写入检查点数据(例如堆栈跟踪)
- 将数据从磁盘存储复制到单独的记录文件
如果有一种方法,可以在不创建新记录文件的情况下,从磁盘存储库中读取正在记录的数据,就可以避免上述开销。
描述
jdk.jfr 模块里的 jdk.jfr.consumer 包,提供了异步订阅事件的功能。用户可以直接从磁盘存储库读取记录数据,也可以直接从磁盘存储流中读取数据,而无需转储记录文件。可以通过注册处理器(例如 lambda 函数)与流交互,从而对事件的到达进行响应。
下面的例子打印 CPU 的总体使用率,并持有锁 10 毫秒。
try (var rs = new RecordingStream()) {
rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10));
rs.onEvent("jdk.CPULoad", event -> {
System.out.println(event.getFloat("machineTotal"));
});
rs.onEvent("jdk.JavaMonitorEnter", event -> {
System.out.println(event.getClass("monitorClass"));
});
rs.start();
}
RecordingStream 类实现了接口 jdk.jfr.consumer.EventStream,该接口提供了一种统一的方式来过滤和使用事件,无论源是实时流还是磁盘上的文件。
public interface EventStream extends AutoCloseable {
public static EventStream openRepository();
public static EventStream openRepository(Path directory);
public static EventStream openFile(Path file);
void setStartTime(Instant startTime);
void setEndTime(Instant endTime);
void setOrdered(boolean ordered);
void setReuse(boolean reuse);
void onEvent(Consumer<RecordedEvent> handler);
void onEvent(String eventName, Consumer<RecordedEvent handler);
void onFlush(Runnable handler);
void onClose(Runnable handler);
void onError(Runnable handler);
void remove(Object handler);
void start();
void startAsync();
void awaitTermination();
void awaitTermination(Duration duration);
void close();
}
创建流的方法有 3 种:
- EventStream::openRepository(Path) 从磁盘存储库中构造一个流。这是一种可以直接通过文件系统监视其他进程的方法。磁盘存储库的位置存储在系统属性
jdk.jfr.repository中,可以使用 API 读取到。 - EventStream::openRepository() 方法执行进程内监控。与 RecordingStream 不同,它不会开始录制。相反,仅当通过外部方式(例如,使用 JCMD 或 JMX)启动记录时,流才接收事件。
- EventStream::openFile(Path) 从记录文件中创建流,扩充了已经存在的
RecordingFile类。
该接口还可用于设置缓冲的数据量,以及是否应按时间顺序对事件进行排序。为了最大程度地降低分配压力,还可以选择控制是否应为每个事件分配新的事件对象,或者是否可以重用以前的对象。我们可以在当前线程中启动流,也可以异步启动流。
Java 虚拟机(JVM)每秒一次将线程本地缓冲区中存储的事件定期刷新到磁盘存储库。一个单独的线程解析最近的文件,直到写入数据为止,然后将事件推送给订阅者。为了保持较低的开销,仅从文件中读取活动订阅的事件。要在刷新完成后收到通知,可以使用 EventStream :: onFlush(Runnable)方法注册处理程序。这是在 JVM 准备下一组事件时将数据聚合或推送到外部系统的机会。
Java 虚拟机(JVM)每隔一秒钟就会将线程的本地缓冲区存储的事件刷新到磁盘存储库中。解析文件使用的是一个独立的线程,这个线程解析后将事件推送给订阅者。为了保持较低的开销,仅从文件中读取活跃的订阅事件。若想在一次刷新完成后收到通知,可以使用 EventStream::onFlush(Runnable) 方法注册处理程序。
352: 非原子性的字节缓冲区映射
JEP 352: Non-Volatile Mapped Byte Buffers
添加新的特定于 JDK 的文件映射模式,以便可以使用 FileChannel API 创建引用非易失性存储器的 MappedByteBuffer 实例。
动机
非易失性存储器 (non-volatile memory,NVM) 可以使程序在运行过程中创建和更新程序状态,而减少持久性介质的大量复制 / 翻译成本。这对于事务程序非常重要,事务程序需要定期保持不确定状态的持久性,以便在崩溃后恢复程序状态。
现有的 C 库(例如 Intel 的 libpmem)为 C 程序提供了对 NVM 的高效访问。现在在 Java 中调用基本库代价也是非常大的,因为这需要频繁进行系统调用或 JNI 调用,同时 C 语言中提供的持久数据类型分配在无法从 Java 直接访问的内存中。Java 应用程序和中间件(例如 Java 事务管理器)与 C 或可以低成本链接到 C 库的语言相比,处于严重的劣势。
该特性试图通过允许映射到 ByteBuffer 的 NVM 的有效回写解决第一个问题。由于 Java 可以直接访问 ByteBuffer 映射的内存,因此这可以通过实现与 C 语言中提供的客户端库等效的客户端库来解决第二个问题,以管理不同持久数据类型的存储。
描述
初步变更
该 JEP 使用了 Java SE API 的两个增强功能:
- 支持 implementation-defined 的映射模式
- MppedByteBuffer::force 方法以指定范围
特定于 JDK 的 API 更改
- 通过新模块中的公共 API 公开新的 MapMode 枚举值
一个公共扩展枚举 ExtendedMapMode 将添加到 jdk.nio.mapmode 程序包:
package jdk.nio.mapmode;
. . .
public class ExtendedMapMode {
private ExtendedMapMode() {}
public static final MapMode READ_ONLY_SYNC = . . .
public static final MapMode READ_WRITE_SYNC = . . .
}
在调用 FileChannel::map 方法创建映射到 NVM 设备文件上的只读或读写 MappedByteBuffer 时,可以使用上述的枚举值。如果这些标志在不支持 NVM 设备文件的平台上传递,程序会抛出 UnsupportedOperationException 异常。在受支持的平台上,仅当目标 FileChannel 实例是从通过 NVM 设备打开的派生文件时,才能传递这些参数。在任何其他情况下,都会抛出 IOException 异常。
- 发布
BufferPoolMXBean,用于跟踪 MappedByteBuffer 统计信息
初步商定的内部 JDK API 更改
- 向类
jdk.internal.misc.Unsafe中添加新的writebackMemory方法
358: 友好的空指针异常
JEP 358: Helpful NullPointerExceptions
精确描述哪个变量为 null,提高 JVM 生成的 NullPointerExceptions 的可用性。
动机
每个 Java 开发人员都遇到过 NullPointerExceptions (NPEs) 问题。NPE 几乎可以出现在程序的任意位置,因此尝试捕获和修复它们是不可能的。下面的代码:
a.i = 99;
JVM 会打印出方法名、文件名和 NPE 异常的行数:
Exception in thread "main" java.lang.NullPointerException
at Prog.main(Prog.java:5)
使用这个错误报告,开发人员可以定位到 a.i = 99; 并推断对象 a 是 null。但是对于更复杂的代码,不使用调试器就无法确定哪个变量为空。假设下面的代码中出现了一个 NPE:
a.b.c.i = 99;
仅仅使用文件名和行数,并不能精确定位到哪个变量为 null,是 a、b 还是 c?
访问数组也会发生类似的问题。假设此代码中出现一个 NPE:
a[i][j][k] = 99;
文件名和行号不能精确指出哪个数组组件为空。是 a 还是 a [i] 或 a [i] [j]?
一行代码可能包含多个访问路径,每个访问路径都可能是 NPE 的来源。假设此代码中出现一个 NPE:
a.i = b.j;
文件名和行号并不能确定哪个对象为空,是 a 还是 b?
NPE 也可能在方法调用中传递,看下面的代码:
x().y().i = 99;
文件名和行号不能指出哪个方法调用返回 null。是 x() 还是 y()?
描述
JVM 在程序调用空引用的位置抛出 NPE 异常,通过分析程序的字节码指令,JVM 可以精确判断哪个变量为空,并在 NPE 中描述详细信息(根据源代码)。包含方法名、文件名和行号的 null-detail 消息将显示在 JVM 的消息中。
例如 a.i = 99; 的 NPE 异常可能是如下格式:
Exception in thread "main" java.lang.NullPointerException:
Cannot assign field "i" because "a" is null
at Prog.main(Prog.java:5)
在更复杂的 a.b.c.i = 99; 语句中,NPE 消息会包含导致空值的完整访问路径:
Exception in thread "main" java.lang.NullPointerException:
Cannot read field "c" because "a.b" is null
at Prog.main(Prog.java:5)
同样,如果数组访问和赋值语句 a[i][j][k] = 99; 引发 NPE:
Exception in thread "main" java.lang.NullPointerException:
Cannot load from object array because "a[i][j]" is null
at Prog.main(Prog.java:5)
类似地,a.i = b.j; 会引发 NPE:
Exception in thread "main" java.lang.NullPointerException:
Cannot read field "j" because "b" is null
at Prog.main(Prog.java:5)
359: Records (预览)
通过 Records(不知道如何翻译,囧……)增强 Java 编程语言。Records 提供了一种紧凑的语法来声明类,这些类是浅层不可变数据的透明持有者。
动机
我们经常听到这样的抱怨:“Java 太冗长”、“Java 规则过多”。首当其冲的就是充当简单集合的 “数据载体” 的类。为了写一个数据类,开发人员必须编写许多低价值、重复且容易出错的代码:构造函数、访问器、equals()、hashCode()和 toString()等等。
尽管 IDE 可以帮助开发人员编写数据载体类的绝大多数编码,但是这些代码仍然冗长。
从表面上看,将 Records 是为了简化模板编码而生的,但是它还有 “远大” 的目标:modeling data as data。records 应该更简单、简洁、数据不可变。
描述
records 是 Java 的一种新的类型。同枚举一样,records 也是对类的一种限制。records 放弃了类通常享有的特性:将 API 和表示解耦。但是作为回报,records 使数据类变得非常简洁。
一个 record 具有名称和状态描述。状态描述声明了 record 的组成部分。例如:
record Point(int x, int y) { }
因为 records 在语义上是数据的简单透明持有者,所以记录会自动获取很多标准成员:
- 状态声明中的每个成员,都有一个 private final 的字段;
- 状态声明中的每个组件的公共读取访问方法,该方法和组件具有相同的名字;
- 一个公共的构造函数,其签名与状态声明相同;
- equals 和 hashCode 的实现;
- toString 的实现。
限制
records 不能扩展任何类,并且不能声明私有字段以外的实例字段。声明的任何其他字段都必须是静态的。
records 类都是隐含的 final 类,并且不能是抽象类。这些限制使得 records 的 API 仅由其状态描述定义,并且以后不能被其他类实现或继承。
在 record 中额外声明变量
也可以显式声明从状态描述自动派生的任何成员。可以在没有正式参数列表的情况下声明构造函数(这种情况下,假定与状态描述相同),并且在正常构造函数主体正常完成时调用隐式初始化(this.x=x)。这样就可以在显式构造函数中仅执行其参数的验证等逻辑,并省略字段的初始化,例如:
record Range(int lo, int hi) {
public Range {
if (lo> hi) /* referring here to the implicit constructor parameters */
throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
}
}
语法
RecordDeclaration:
{ClassModifier} record TypeIdentifier [TypeParameters]
(RecordComponents) [SuperInterfaces] [RecordBody]
RecordComponents:
{RecordComponent {, RecordComponent}}
RecordComponent:
{Annotation} UnannType Identifier
RecordBody:
{{RecordBodyDeclaration} }
RecordBodyDeclaration:
ClassBodyDeclaration
RecordConstructorDeclaration
RecordConstructorDeclaration:
{Annotation} {ConstructorModifier} [TypeParameters] SimpleTypeName
[Throws] ConstructorBody
反射 API
下面的方法会被加到 java.lang.Class 中:
- RecordComponent[] getRecordComponents()
- boolean isRecord()
361: Switch 表达式 (标准)
JEP 361: Switch Expressions (Standard)
扩展 switch 可以使其应用于语句或表达式。
动机
当我们准备增强 Java 编程语言以支持模式匹配(JEP 305)时,现有 switch 语句的一些不规则性(长期以来一直困扰着用户)成为了障碍。下面的代码中,众多的 break 语句使代码变得冗长,这种 “视觉噪声” 通常掩盖了更多的错误。
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
System.out.println(6);
break;
case TUESDAY:
System.out.println(7);
break;
case THURSDAY:
case SATURDAY:
System.out.println(8);
break;
case WEDNESDAY:
System.out.println(9);
break;
}
我们建议引入一种新形式的 switch 标签 case L->,以表示如果匹配标签,则只执行标签右边的代码。switch 标签允许在每种情况下使用逗号分隔多个常量。现在可以这样编写以前的代码:
switch (day) {
case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
case TUESDAY -> System.out.println(7);
case THURSDAY, SATURDAY -> System.out.println(8);
case WEDNESDAY -> System.out.println(9);
}
switch 标签 case L-> 右侧的代码被限制为表达式、代码块或 throw 语句。这样局部变量的范围在本块之内,而传统的 switch 语句局部变量的作用域是整个模块!
switch (day) {
case MONDAY:
case TUESDAY:
int temp = ... // The scope of 'temp' continues to the }
break;
case WEDNESDAY:
case THURSDAY:
int temp2 = ... // Can't call this variable'temp'
break;
default:
int temp3 = ... // Can't call this variable'temp'
}
许多现有的 switch 语句实质上是对 switch 表达式的模拟,其中每个分支要么分配给一个公共目标变量,要么返回一个值:
int numLetters;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
numLetters = 6;
break;
case TUESDAY:
numLetters = 7;
break;
case THURSDAY:
case SATURDAY:
numLetters = 8;
break;
case WEDNESDAY:
numLetters = 9;
break;
default:
throw new IllegalStateException("Wat:" + day);
}
上面的表述是复杂、重复且容易出错的。代码设计者的意图是为每天计算 numLetters。这段代码可以改写成下面这段形式,更加清晰和安全:
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
};
描述
Arrow labels
除了传统的 case L : 标签外,还定义了一种更简洁的形式:case L -> 标签。如果表达式匹配了某个标签,则仅执行箭头右侧的表达式或语句;否则将不执行任何操作。
static void howMany(int k) {
switch (k) {
case 1 -> System.out.println("one");
case 2 -> System.out.println("two");
default -> System.out.println("many");
}
}
下面的代码:
howMany(1);
howMany(2);
howMany(3);
将会打印:
one
two
many
Switch expressions
JDK 14 扩展了 switch 语句,使其可以应用于表达式中。例如上述的 howMany 方法可以重写为如下形式:
static void howMany(int k) {
System.out.println(
switch (k) {
case 1 -> "one";
case 2 -> "two";
default -> "many";
}
);
}
在通常情况下,switch 表达式如下所示:
T result = switch (arg) {
case L1 -> e1;
case L2 -> e2;
default -> e3;
};
Yielding a value
大多数 switch 表达式在 case L-> 标签的右侧都有一个表达式。如果需要一个完整的块,JDK 14 引入了一个新的 yield 语句来产生一个值,该值成为封闭的 switch 表达式的值。
int j = switch (day) {
case MONDAY -> 0;
case TUESDAY -> 1;
default -> {
int k = day.toString().length();
int result = f(k);
yield result;
}
};
363: 移除 CMS(Concurrent Mark Sweep)垃圾收集器
JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
动机
在两年多以前的 JEP 291 中,我们就已经弃用了 CMS 收集器,并说明会在以后的发行版中删除,以加快其他垃圾收集器的发展。在这段时间里,我们看到了 2 个新的垃圾收集器 ZGC 和 Shenandoah 的诞生,同时对 G1 的进一步改进。G1 自 JDK 6 开始便成为 CMS 的继任者。我们希望以后现有的收集器进一步减少对 CMS 的需求。
描述
此更改将禁用 CMS 的编译,删除源代码中 gc/cms 目录的内容,并删除仅与 CMS 有关的选项。尝试使用命令 -XX:+UseConcMarkSweepGC 开启 CMS 会收到以下警告:
Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; \
support was removed in <version>
VM 将使用默认收集器继续执行。
364: macOS 系统上的 ZGC
将 ZGC 垃圾收集器移植到 macOS。
动机
尽管我们希望需要 ZGC 可伸缩性的用户使用基于 Linux 的环境,但是在部署应用程序之前,开发人员通常会使用 Mac 进行本地开发和测试。 还有一些用户希望运行桌面应用程序,例如带有 ZGC 的 IDE。
尽管我们希望需要 ZGC 可伸缩性的用户都是基于 Linux 环境的,但是在部署应用程序之前,开发人员通常会使用 Mac 进行本地开发和测试。 还有一些用户希望运行桌面应用程序,例如带有 ZGC 的 IDE。
描述
ZGC 的 macOS 实现由两部分组成(下面是机翻,写不动了……):
- 支持 macOS 上的多映射内存。 ZGC 设计大量使用彩色指针,因此在 macOS 上我们需要一种将多个虚拟地址(在算法中包含不同颜色)映射到同一物理内存的方法。我们将为此使用 mach microkernel mach_vm_remap API。堆的物理内存在单独的地址视图中维护,在概念上类似于文件描述符,但位于(主要是)连续的虚拟地址中。该内存被重新映射到内存的各种 ZGC 视图中,代表了算法的不同指针颜色。
- ZGC 支持不连续的内存保留。在 Linux 上,我们在初始化期间保留 16TB 的虚拟地址空间。我们假设没有共享库将映射到所需的地址空间。在默认的 Linux 配置上,这是一个安全的假设。但是在 macOS 上,ASLR 机制会侵入我们的地址空间,因此 ZGC 必须允许堆保留不连续。假设 VM 实现使用单个连续的内存预留,则共享的 VM 代码也必须停止。如此一来,is_in_reserved(),reserved_region()和 base()之类的 GC API 将从 CollectedHeap 中删除。
弃用 ParallelScavenge + SerialOld GC 组合
JEP 366: Deprecate the ParallelScavenge + SerialOld GC Combination
动机
有一组 GC 算法的组合很少使用,但是维护起来却需要巨大的工作量:并行年轻代 GC(ParallelScavenge)和串行老年代 GC(SerialOld)的组合。用户必须使用 -XX:+UseParallelGC -XX:-UseParallelOldGC 来启用此组合。
这种组合是畸形的,因为它将并行的年轻代 GC 算法和串行的老年代 GC 算法组合在一起使用。我们认为这种组合仅在年轻代很多、老年代很少时才有效果。在这种情况下,由于老年代的体积较小,因此完整的收集暂停时间是可以接受的。但是在生产环境中,这种方式是非常冒险的:年轻代的对象容易导致 OutOfMemoryException。此组合的唯一优势是总内存使用量略低。我们认为,这种较小的内存占用优势(最多是 Java 堆大小的约 3%)不足以超过维护此 GC 组合的成本。
描述
除了弃用选项组合 -XX:+UseParallelGC -XX:-UseParallelOldGC 外,我们还将弃用选项 -XX:UseParallelOldGC,因为它唯一的用途是取消选择并行的旧版 GC,从而启用串行旧版 GC。
因此,任何对 UseParallelOldGC 选项的明确使用都会显示弃用警告。
368: 文本块 (第二个预览版)
JEP 368: Text Blocks (Second Preview)
Java 语言增加文本块功能。文本块是多行字符串文字,能避免大多数转义。
动机
在 Java 中,HTML, XML, SQL, JSON 等字符串对象都很难阅读和维护。
HTML
使用 one-dimensional 的字符串语法:
String html = "<html>\n" +
"<body>\n" +
"<p>Hello, world</p>\n" +
"</body>\n" +
"</html>\n";
使用 two-dimensional 文本块语法:
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
SQL
使用 one-dimensional 的字符串语法:
String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
"WHERE `CITY` ='INDIANAPOLIS'\n" +
"ORDER BY `EMP_ID`, `LAST_NAME`;\n";
使用 two-dimensional 文本块语法:
String query = """
SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
WHERE `CITY` = 'INDIANAPOLIS'
ORDER BY `EMP_ID`, `LAST_NAME`;
""";
多语言示例
使用 one-dimensional 的字符串语法:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("function hello() {\n" +
"print('\"Hello, world\"');\n" +
"}\n" +
"\n" +
"hello();\n");
使用 two-dimensional 文本块语法:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("""
function hello() {
print('"Hello, world"');
}
hello();
""");
描述
文本块是 Java 语言的新语法,可以用来表示任何字符串,具有更高的表达能力和更少的复杂度。
文本块的开头定界符是由三个双引号字符(””“)组成的序列,后面跟 0 个或多个空格,最后跟一个行终止符。内容从开头定界符的行终止符之后的第一个字符开始。
结束定界符是三个双引号字符的序列。内容在结束定界符的第一个双引号之前的最后一个字符处结束。
与字符串文字中的字符不同,文本块的内容中可以直接包含双引号字符。允许在文本块中使用 \“,但不是必需的或不建议使用。
与字符串文字中的字符不同,内容可以直接包含行终止符。允许在文本块中使用 \ n,但不是必需或不建议使用。例如,文本块:
"""
line 1
line 2
line 3
"""
等效于字符串文字:
"line 1\nline 2\nline 3\n"
或字符串文字的串联:
"line 1\n" +
"line 2\n" +
"line 3\n"
370: 外部存储器 API (Incubator)
JEP 370: Foreign-Memory Access API (Incubator)
引入一个 API,以允许 Java 程序安全有效地访问 Java 堆之外的外部内存。
动机
许多 Java 的库都能访问外部存储,例如 Ignite, mapDB, memcached, and Netty’s ByteBuf API。这样可以:
- 避免垃圾回收相关成本和不可预测性
- 跨多个进程共享内存
- 通过将文件映射到内存中来序列化、反序列化内存内容。
但是 Java API 却没有提供一个令人满意的访问外部内存的解决方案。
Java 1.4 中引入的 ByteBuffer API 允许创建直接字节缓冲区,这些缓冲区是按堆外分配的,并允许用户直接从 Java 操作堆外内存。但是,直接缓冲区是有限的。
开发人员可以从 Java 代码访问外部内存的另一种常见途径是使用 sun.misc.Unsafe API。Unsafe 有许多公开的内存访问操作(例如 Unsafe::getInt 和 putInt)。使用 Unsafe 访问内存非常高效:所有内存访问操作都定义为 JVM 内在函数,因此 JIT 会定期优化内存访问操作。然而根据定义,Unsafe API 是不安全的——它允许访问任何内存位置(例如,Unsafe::getInt 需要很长的地址)。如果 Java 程序了访问某些已释放的内存位置,可能会使 JVM 崩溃。最重要的是,Unsafe API 不是受支持的 Java API,并且强烈建议不要使用它。
虽然也可以使用 JNI 访问内存,但是与该解决方案相关的固有成本使其在实践中很少适用。整个开发流程很复杂,因为 JNI 要求开发人员编写和维护 C 代码段。 JNI 本质上也很慢,因为每次访问都需要 Java 到 native 的转换。
在访问外部内存时,开发人员面临一个难题:应该使用安全但受限(可能效率较低)的方法(例如 ByteBuffer),还是应该放弃安全保证并接受不受支持和危险的 Unsafe API?
该 JEP 引入了受支持的,安全且有效的外部内存访问 API。并且设计时就充分考虑了 JIT 优化。
描述
外部存储器访问 API 引入了三个主要的抽象:MemorySegment,MemoryAddress 和 MemoryLayout。
MemorySegment 用于对具有给定空间和时间范围的连续内存区域进行建模。可以将 MemoryAddress 视为段内的偏移量,MemoryLayout 是内存段内容的程序描述。
可以从多种来源创建内存段,例如本机内存缓冲区,Java 数组和字节缓冲区(直接或基于堆)。例如,可以如下创建本机内存段:
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
...
}
上述代码将创建大小为 100 字节的,与本机内存缓冲区关联的内存段。
内存段在空间上受限制;任何试图使用该段来访问这些界限之外的内存的尝试都会导致异常。正如使用 try-with-resource 构造所证明的那样,片段在时间上也是有界的。也就是说,它们已创建,使用并在不再使用时关闭。关闭段始终是一个显式操作,并且可能导致其他副作用,例如与该段关联的内存的重新分配。任何访问已关闭的内存段的尝试都将导致异常。空间和时间安全性检查对于确保内存访问 API 的安全性至关重要。
通过获取内存访问 var 句柄可以取消引用与段关联的内存。这些特殊的 var 句柄具有至少一个强制访问坐标,类型为 MemoryAddress,即发生取消引用的地址。它们是使用 MemoryHandles 类中的工厂方法获得的。要设置本机段的元素,我们可以使用如下所示的内存访问 var 句柄:
VarHandle intHandle = MemoryHandles.varHandle(int.class);
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
MemoryAddress base = segment.baseAddress();
for (int i = 0 ; i < 25 ; i++) {
intHandle.set(base.offset(i * 4), i);
}
}
总结
对普通 Java 开发人员来说,最重要的无疑是 Record 类、switch 语句、文本块。新兴语言(例如 Kotlin)早就实现了这些特性——语法更加简洁、特性更加丰富,在这些语言面前,曾经新兴的 Java 也变成了 “老古董”。
实际开发过程中,Lombok 插件和 Spring Boot 能大大提高 Java 的开发效率。Java 近些年的版本迭代也是有目共睹的,虽然新版本特性更多、稳定性和安全性更高,但是目前企业开发仍以 Java 8 居多,中小企业的 JDK 升级意愿并不强烈。