OpenJDK12的新特性(上)

OpenJDK.jpg

文章摘要

OpenJDK 12是由JSR 386在Java Community Process中指定的Java SE平台的版本12的开源参考实现,于2019年3月19日达到General Availability版本。GPL(General Public License)协议下的生产就绪二进制文件可从Oracle获得;其他供应商的二进制文件很快就会出现。

该版本的功能和时间表是通过JEP流程提出和跟踪的,并由JEP 2.0提案进行了修订。该版本使用JDK Release Process(JEP 3)生成发布。

本文根据OpenJDK 12的官方文档:OpenJDK 12,对其新特性进行整理,受本人翻译水平所限,难免有翻译或理解错误,望不吝指正。

特性

JEP Features
189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental)
230: Microbenchmark Suite
325: Switch Expressions (Preview)
334: JVM Constants API
340: One AArch64 Port, Not Two
341: Default CDS Archives
344: Abortable Mixed Collections for G1
346: Promptly Return Unused Committed Memory from G1

注意:因篇幅限制,下文将对OpenJDK 12的新特性:189、230、325、334进行整理,剩余特性的整理请见下篇:OpenJDK12的新特性(下)

JEP189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental)

低GC停顿的垃圾收集器(Experimental版本)

摘要

添加一个名为Shenandoah的新垃圾收集(GC)算法,在执行GC动作时可以通过并发的方式让Java程序继续执行,从而缩短Stop_The_World的时间。使用Shenandoah的停顿时间与堆大小无关,这意味着无论堆是200 MB还是200 GB,都将具有相同的稳定停顿时间。

非目标

这个GC算法并不是万能的,还有其他的垃圾收集算法会优先考虑吞吐量或者内存占用,而不是响应性。Shenandoah算法适用于那些注重响应性和可预测的停顿时间的应用,它并不能解决所有的JVM停顿问题,那些由于GC之外的其他原因,如达到安全点的时间(TTSP)或者监控通胀,导致的停顿时间超出了此JEP的范围。

成功标准

保持一致的较短的GC停顿时间。

描述

现代机器拥有比以往更多的内存和更多的处理器。服务水平协议(SLA)应用程序保证响应时间为10-500毫秒。为了满足该目标的低端,我们需要足够高效垃圾收集算法以允许程序在可用内存中运行,而且也要能做到永远不会中断正在运行的程序超过几毫秒。 Shenandoah是OpenJDK开源的低停顿收集器,旨在让我们更接近这些目标。

Shenandoah通过牺牲并发cpu周期和空间以优化停顿时间。我们为每个Java对象添加了一个间接指针,使得GC线程进行Java堆整理时能够与用户线程并发执行。由于整个GC过程中耗时最长的并发标记和整理过程收集器线程与用户线程可以一起工作,所以只需要在扫描线程堆栈以枚举根节点时停顿Java执行线程。

Shenandoah算法在this PPPJ2016 paper中有详细描述。

Shenandoah已经应用并且将由Red Hat提供aarch64和amd64支持。

在OpenJDK Shenandoah项目中的Shenandoah开发已经完成。在Shenandoah wiki page页面上查看有关当前开发流程,实现细节和可用性的更多详细信息。

选择

Zing/Azul有一个更低停顿的收集器,但是这项工作还没有贡献给OpenJDK。

ZGC有基于彩色指针的低停顿收集器, 我们期待比较两种策略的表现。

G1执行一些平行和并发工作,但它不执行并发回收(编者注:G1的筛选回收阶段,从Sun公司透漏出的信息来看,其实是支持与用户线程并发执行的,但是由于只会有一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率)。

CMS执行并发标记,但它在停顿时执行年轻代复制,并且从不整理老年代, 这导致花费更多时间来管理老年代中的可用空间以及碎片问题(编者注:CMS是基于“标记-清除”算法实现的收集器,这就意味着收集结束的时候将有大量的空间碎片产生)。

建立和调用

作为实验性功能,Shenandoah要求在命令行中配置-XX:+UnlockExperimentalVMOptions参数,构建系统会自动禁用不受支持的配置。下游构建者可以选择在其他支持的平台上使用--with-jvm-features = -shenandoahgc禁用构建Shenandoah。

要启用/使用Shenandoah GC,需要使用以下JVM选项:-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

想要获取有关如何设置和调整Shenandoah GC的更多信息,请参阅Shenandoah wiki页面。

测试

Red Hat已经为我们的重要应用程序进行了大量测试。我们开发了许多Shenandoah特定的jtreg测试。Shenandoah从Fedora 24开始在Fedora中传送,并在Rhel 7.4中作为技术预览。使用-XX:+UseShenandoahGC运行标准的OpenJDK测试就足够了。

风险和假设

GC接口(JEP 304)已集成在JDK 11中,之后对GC接口进行了许多扩展和改进,这样可以最大限度地降低将Shenanodah添加到OpenJDK源代码库的风险。除此之外,任何无法合理编址的Shenandoah-specific代码路径都将受到#ifdef INCLUDE_SHENANDOAHGC或类似机制的保护。Shenandoah GC最初将被标记为实验性功能,因此在配置参数方面除-XX:+UseShenandoahGC之外还需要-XX:+UnlockExperimentalVMOptions

JEP230: Microbenchmark Suite

Microbenchmark 套件

摘要

在JDK源代码中添加一套基本的microbenchmarks ,使开发人员可以轻松运行现有的microbenchmarks 并创建新的microbenchmarks 。

目标

  • 基于[Java Microbenchmark Harness(JMH)];[1]
  • 稳定且经过调整的benchmarks,针对持续性能测试
    ​ ​ - 在feature release以及non-feature releases的Feature Complete milestone之后提供稳定且不移动的套件
    ​ ​ - 支持与先前JDK版本的适用测试比较
  • 简易
    • 轻松添加新benchmarks
    • 在APIs和options更改、不推荐使用或在开发期间删除时,可以轻松更新测试
    • 易于构建
    • 易于查找和运行benchmark
  • 支持JMH更新
  • 在组件中包含大约一百个benchmarks的初始集

非目标

  • 为新的JDK功能提供benchmarks不是目标,为新功能添加benchmarks是这些项目的一部分。

  • 创建一套完整的benchmarks来覆盖JDK中所有内容不是目标。随着时间的推移,该套件将继续通过新编写的benchmarks,或通过专门针对扩展其覆盖范围的协作进行扩展。

  • 为处理microbenchmarks中的二进制依赖提供解决方案不是目标,稍后可能会添加对此的支持。

描述

microbenchmark套件将与JDK源代码位于一个目录下,并且在构建时将生成单个JAR文件。协同定位将简化在开发期间添加和定位benchmarks。在运行benchmarks时,JMH提供强大的过滤功能,允许用户仅运行当前感兴趣的benchmarks,确切的位置仍有待确定。

Benchmarking 通常需要与早期build甚至release版本进行比较,因此microbenchmarks 必须支持JDK(N),用于针对新JDK和JDK(N-1)中的特性,以及存在于早期release版本中的特性的benchmarks 。 这意味着,对于JDK 12,结构和构建脚本必须支持JDK 12和JDK 11的编译benchmarks 。benchmarks 将进一步使用描述他们正在测试的JDK区域的Java包名称进行划分。

建议使用以下目录结构:

1
2
3
4
5
6
jdk/jdk
.../make/test (Shared folder for Makefiles)
.../test (Shared folder for functional tests)
.../micro/org/openjdk/bench
.../java (subdirectories similar to JDK packages and modules)
.../vm (subdirectories similar to HotSpot components)

microbenchmark套件的构建将与普通的JDK构建系统集成。它将是一个单独的目标,在正常的JDK构建期间不会执行,以便为开发人员和其他对构建微基准套件不感兴趣的人保持较低的构建时间。要构建微基准套件,用户必须专门运行make build-microbenchmark或类似工具。另外,将支持使用make test TEST =“micro:regexp”运行基准测试。有关如何设置本地环境的说明将记录在docs / testing.md | html中。

benchmark将完全依赖于JMH,就像某些单元测试依赖于TestNG或jtreg一样,所以虽然对JMH的依赖是新的,但是构建的其他部分具有相似的依赖性。与jtreg相比的一个区别是JMH在构建期间都被使用,并且被打包为生成的JAR文件的一部分。

microbenchmark套件中的benchmark集将从JMH JDK Microbenchmarks项目中导入。这些构成了一组已在内部使用的经过调整和测试的microbenchmark。一个悬而未决的问题是,是将整个独立项目整体迁移到共处套件,还是将其作为更长寿命回归测试的stabilization forest。

但是,任何用户仍然希望确保其他参数(例如执行机器和JDK)在进行分析时是稳定且可比较的。在通常情况下,我们希望benchmark能够在不到一分钟的时间内完成整个运行。这不是大型或长期运行benchmark的包装框架;目标是提供一套快速且有针对性的benchmark。在某些特殊情况下,benchmark可能需要更长时间的预热或运行时间才能获得稳定的结果,但应尽可能避免这种情况。套件的目标不是充当大型工作负载的通用包装器;相反,意图是从更大的benchmark中提取关键组件或方法,并仅将该部分强调为microbenchmark。

作为该项目的一部分,将创建wiki.openjdk.java.net上的新页面,以帮助解释如何开发新benchmarks并描述添加benchmark的要求。这些要求将要求遵守编码标准、可重现的性能,以及明确的benchmark测量文档及其测量内容。

选择

继续将microbenchmark套件维护为一个单独的项目[2]

协同定位简化了为新功能添加benchmark,特别是在所有新功能开发的大部分在项目存储库(Valhalla,Amber等)中完成的世界中。 在单独的项目模型中被证明特别复杂的情况是测试对javac本身的更改,需要使用每个相应的JDK显式重建benchmark套件。 协同定位可以更优雅地解决这个特定用例,同时不禁止使用预先构建的benchmark捆绑包来在较长时间段内对稳定测试进行性能跟踪。

测试

作为常规性能测试的一部分,性能团队将验证microbenchmark测试,以确保仅添加稳定、调整好的和准确的microbenchmark测试。 还将根据具体情况对基准进行评估和分析,以确保其测试预期的功能。所有测试必须在所有适用的平台上运行多次,以确保它们稳定。

JEP325: Switch Expressions (Preview)

摘要

扩展switch语句,以便它可以用作语句或表达式,并且这两种形式都可以使用“traditional”或“simplified”的作用域,都可以实现对程序的流程控制。 这些更改将简化日常编码,并为在switch语句中使用模式匹配(JEP 305)做好准备。

动机

当我们准备增强Java编程语言以支持模式匹配(JEP 305)时,现有switch语句的一些不规则性(长期以来一直是用户的烦恼)成为我们工作的障碍。这包括switch blocks的默认控制流行为(fall through),switch blocks的默认作用域,还包括对switch使用时仅作为语句,尽管作为表达式的话实现multi-way conditionals通常更自然。

当前Java的switch语句设计紧随C和C++等语言,并且默认支持fall-through语义。虽然这种传统的控制流通常用于编写low-level代码(例如用于二进制编码的解析器),但随着switch用于higher-level的contexts,其error-prone的性质开始超过其灵活性。

例如,在下面的代码中,许多break语句使它不必要地冗长,并且这种视觉干扰经常掩盖难以debug的错误,比如缺少break语句将导致发生意外的fall-through错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 label,写成“case L - >”,用来表示如果标签匹配则只执行标签右侧的代码。 例如, 前面的代码现在可以写成下面这种形式:

1
2
3
4
5
6
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);
}

(此示例还使用多个case 标签:我们支持在单个switch label中多个标签以逗号分隔开。)

“case L - >”开关标签右侧的代码仅限于表达式、块或(为方便起见)throw语句。 这具有令人满意的结果,如果一条分支(arm)引入局部变量,则它必须包含在该分支的块中,不在 switch block中的任何其他分支的作用域内。 这消除了“traditional”作用域的switch block的另一个烦恼,即局部变量的范围是整个switch block。

1
2
3
4
5
6
7
8
9
10
11
12
switch (day) {
case MONDAY:
case TUESDAY:
int temp = ...
break;
case WEDNESDAY:
case THURSDAY:
int temp2 = ... // Why can't I call this temp?
break;
default:
int temp3 = ... // Why can't I call this temp?
}

许多现有的switch语句本质上是switch表达式的模拟,其中每个arm分配给一个公共目标变量或返回一个值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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赋一个值。直说的话,使用一个switch表达式将更清晰、更安全:

1
2
3
4
5
6
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
};

反过来,将switch扩展到支持表达式会引发一些额外的需求,例如扩展流分析(表达式必须始终计算值或突然完成),并允许switch表达式的某些case arm抛出异常而不是产生值。

描述

除了“traditional”的switch block之外,我们还建议添加一个新的“simplified”形式,使用新的“case L - >”switch标签。如果标签匹配,则只执行箭头标签右侧的表达式或语句,并且没有fall through。例如,给定方法:

1
2
3
4
5
6
7
8
9
10
11
static void howMany(int k) {
switch (k) {
case 1 -> System.out.println("one");
case 2 -> System.out.println("two");
case 3 -> System.out.println("many");
}
}

howMany(1);
howMany(2);
howMany(3);

得到以下输出:

1
2
3
one
two
many

我们将扩展switch语句,以便它可以另外作为表达式来使用。 在常见情况下,switch表达式如下所示:

1
2
3
4
5
T result = switch (arg) {
case L1 -> e1;
case L2 -> e2;
default -> e3;
};

switch表达式是多边表达式,如果目标类型已知,则将此类型下推到每个arm中。如果已知,switch表达式的类型是其目标类型;如果未知,则通过组合每个case arm的类型来计算独立的类型。

大多数switch表达式在“case L - >”开关标签的右侧都有一个表达式。 如果需要一个完整的块,我们扩展了break语句以获取一个参数,该参数成为封闭的switch表达式的值。

1
2
3
4
5
6
7
8
9
int j = switch (day) {
case MONDAY -> 0;
case TUESDAY -> 1;
default -> {
int k = day.toString().length();
int result = f(k);
break result;
}
};

与switch语句一样,switch表达式也可以使用带有“case L:”switch标签的“traditional”switch block。在这种情况下,将使用break with value语句生成值:

1
2
3
4
5
6
7
8
9
int result = switch (s) {
case "Foo":
break 1;
case "Bar":
break 2;
default:
System.out.println("Neither Foo nor Bar, hmmm...");
break 0;
};

两种形式的break(有值和无值)类似于方法中return的两种形式。两种形式的return都会立即终止方法的执行;在非void方法中,还必须提供一个值,该值被赋予方法的调用者。

switch表达式的case必须是详尽的,对于任何可能的值,必须有匹配的switch标签。实际上,这通常意味着只需要一个default子句;但是,如果枚举switch表达式涵盖了所有已知情况,default子句可以由编译器插入,即枚举定义在编译时和运行时之间发生了变化。

此外,switch表达式必须正常使用值完成,或抛出异常。编译器检查每个switch标签是否匹配,然后可以产生一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int i = switch (day) {
case MONDAY -> {
System.out.println("Monday");
// ERROR! Block doesn't contain a break with value
}
default -> 1;
};
i = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY:
break 0;
default:
System.out.println("Second half of the week");
// ERROR! Group doesn't contain a break with value
};

控制语句、break、return和continue不能跳过switch表达式,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
z: 
for (int i = 0; i < MAX_VALUE; ++i) {
int k = switch (e) {
case 0:
break 1;
case 1:
break 2;
default:
continue z;
// ERROR! Illegal jump through a switch expression
};
...
}

同时,我们可以扩展switch以支持先前不允许的原始类型(及其装箱类型),例如float,double和long。

JEP334: JVM Constants API

摘要

引入API来规范key class-file和run-time artifacts(特别是可从常量池加载的常量)的标称描述。

动机

每个Java类文件都有一个常量池,用于存储类中字节码指令的操作数。从广义上讲,常量池中的条目描述了运行时构件(如类和方法)或简单值(如字符串和整数)。所有这些条目都称为可加载常量,因为它们可以作为ldc指令的操作数(“加载常量”)。它们也可能出现在invokedynamic指令的bootstrap方法的静态参数列表中。执行ldc或invokedynamic指令会将可装入常量解析为标准Java类型的“实时”值,例如Class,String或int。

操作类文件的程序需要对字节码指令进行建模,然后再加载可加载的常量。但是,使用标准Java类型来规范可加载常量是不合适的。描述字符串(CONSTANT_String_info条目)的可加载常量可能是可以接受的,因为生成“实时”String对象很简单,但是对于描述类(CONSTANT_Class_info条目)的可加载常量是有问题的,因为生成“实时”类对象依赖于类加载的正确性和一致性。不幸的是,类加载有许多环境依赖和失败模式:所需的类不存在或者请求者可能无法访问;类加载的结果因上下文而异;装载类有副作用;有时类加载可能根本不可能(例如当所描述的类尚不存在或者不可加载时,如在编译那些相同的类期间,或在jlink-time转换期间)。

因此,处理可加载常量的程序如果能够以纯粹的名义符号形式操作类和方法,以及不太知名的构件(如方法句柄和动态计算常量),则会更简单:

  • 字节码解析和生成库必须以符号形式描述类和方法句柄。如果没有标准机制,它们必须求助于ad-hoc机制,无论是描述符类型(如ASM的Handle)还是字符串元组(方法所有者,方法名称,方法描述符),或者ad-hoc(和容易出错)的编码成一个字符串。

  • 如果可以在符号域中工作而不是使用“实时”类和方法句柄,则通过旋转字节码(例如LambdaMetafactory)操作的invokedynamic的bootstraps会更简单。

  • 编译器和脱机转换器(例如jlink插件)需要描述无法加载到正在运行的VM的类的类和成员。编译器插件(例如注释处理器)同样需要用符号术语来描述程序元素。

这些类型的库和工具都将受益于使用单一,标准的方式来描述可加载常量。

描述

我们在新的java.lang.invoke.constant包中定义了一系列基于值的符号引用(JVMS 5.1)类型,能够描述每种可加载常量。符号引用描述了纯粹标称形式的可加载常量,与类加载或可访问性上下文分开。 有些类可以作为自己的符号引用(例如String);对于可链接常量,我们定义了一系列符号引用类型(ClassDesc,MethodTypeDesc,MethodHandleDesc和DynamicConstantDesc),它们包含描述这些常量的标称信息。