当前位置:   article > 正文

JAVA新版本特性(10万字长文详解)完全指导手册_java 版本说明

java 版本说明

目录

1、版本详解

1.1、Java 8 升Java 11

1.1.1、Java 8 升Java 11 重要特性必读

1.1.2、升级JDK11概述

1.1.2.1、JDK 10后版本发布规则?

1.1.2.2、JDK 8 升级到JDK 11 性能提升多少?

1.1.2.3、如何更好的理解从JDK 8 到 JDK 11 升级中带来的重要特性?

1.1.3、语言新特性

1.1.3.1、JDK9 - 允许在接口中使用私有方法

1.1.3.2、JDK10 - 局部变量类型推断

1.1.3.3、JDK11 - 用于 Lambda 参数的局部变量语法

1.1.4、新工具和库更新

1.1.4.1、JDK9 - 集合、Stream 和 Optional更新方法

1.1.4.2、JDK9 - 进程 API (Process Handle)

1.1.4.3、JDK9 - 变量句柄 (Var Handle)

1.1.4.4、JDK9 - I/O 流新特性

1.1.4.5、JDK9 - 改进应用安全性能

1.1.4.6、JDK10 - 根证书认证

1.1.4.7、JDK11 - 标准 HTTP Client 升级

1.1.4.8、JDK11 - 简化启动单个源代码文件的方法

1.1.4.9、JDK11 - 支持 TLS 1.3 协议

1.2、Java 11 升Java 17

1.2.1、Java 11 升Java 17 重要特性必读

1.2.2、升级JDK17概述 

1.2.2.1、JDK 17升级的必要性?

1.2.2.2、JDK 11 升级到JDK 17 性能提升多少?

1.2.2.3、如何更好的理解从JDK 11 到 JDK 17 升级中带来的重要特性?

1.2.3、语言新特性

1.2.3.1、JDK14 - Switch 表达式(JDK 12,13预览,14正式)

1.2.3.2、JDK15 - 文本块(JDK 13,14预览,15正式)

1.2.3.3、JDK16 - instanceof 模式匹配(JDK 14,15预览,16正式)

1.2.3.4、JDK16 - Records类型(JDK 14,15预览,16正式)

1.2.3.5、JDK17 - 密封的类和接口(JDK 15,16预览,17正式)

1.2.4、新工具和库更新

1.2.4.1、JDK13 - Socket API 重构

1.2.4.2、JDK14 - 改进 NullPointerExceptions 提示信息

1.2.4.3、JDK15 - 隐藏类 Hidden Classes

1.2.4.4、JDK15 - DatagramSocket API重构

1.2.4.5、JDK16 - 对基于值的类发出警告

1.2.4.6、JDK17 - 增强的伪随机数生成器

1.2.4.6.1、JDK 17之前如何生成随机数?

1.2.4.6.2、为什么需要增强?

1.2.4.6.3、增强后是什么样的?

1.3、Java 9 新特性详解

1.3.1、Java 9 新特性概述

1.3.2、Java 平台 模块系统

1.3.3、Jshell

1.3.4、集合、Stream 和 Optional

1.3.5、进程 API

1.3.6、平台日志 API 和 服务

1.3.7、反应式流 ( Reactive Streams )

1.3.8、变量句柄

1.3.9、改进方法句柄(Method Handle)

1.3.10、并发

1.3.11、Nashorn

1.3.12、I/O 流新特性

1.3.13、改进应用安全性能

1.3.14、用户界面

1.3.15、统一 JVM 日志

1.3.16、其他改动方面

1.3.17、结束语

1.4、Java 10 新特性概述

1.4.1、Java 10 新特性概述 

1.4.2、局部变量类型推断

1.4.3、整合 JDK 代码仓库

1.4.4、统一的垃圾回收接口

1.4.5、并行全垃圾回收器 G1

1.4.6、应用程序类数据共享

1.4.7、线程-局部管控

1.4.8、移除 Native-Header 自动生成工具

1.4.9、额外的 Unicode 语言标签扩展

1.4.10、备用存储装置上的堆分配

1.4.11、基于 Java 的 实验性 JIT 编译器

1.4.12、根证书认证

1.4.13、基于时间的版本发布模式

1.4.14、结束语

1.5、Java 11 新特性概述

1.5.1、基于嵌套的访问控制

1.5.2、标准 HTTP Client 升级

1.5.3、Epsilon:低开销垃圾回收器

1.5.4、简化启动单个源代码文件的方法

1.5.5、用于 Lambda 参数的局部变量语法

1.5.6、低开销的 Heap Profiling

1.5.7、支持 TLS 1.3 协议

1.5.8、ZGC:可伸缩低延迟垃圾收集器

1.5.9、飞行记录器

 1.5.10、动态类文件常量

1.5.11、结束语

1.6、Java 12 新特性概述

1.6.1、新功能和库的更新

1.6.1.1、JEP334: JVM常量API

1.6.1.2、JEP341: 默认CDS归档

1.6.1.3、JEP230: Microbenchmark测试套件

1.6.2、新的平台支持

1.6.2.1、JEP340: 移除多余ARM64实现

1.6.3、JVM 优化

1.6.3.1、JPE 344: G1的可中断 mixed GC

1.6.3.2、JEP 346: G1归还不使用的内存

1.6.4、新功能的预览和实验

1.6.4.1、JEP 189: Shenandoah:低暂停时间垃圾收集器(实验)

1.6.4.2、JEP 325: Switch 表达式 (预览版本)

1.7、Java 13 新特性概述

1.7.1、新功能和库的更新

1.7.1.1、JEP350: 动态应用程序类-数据共享

1.7.1.2、JEP353: Socket API 重构

1.7.2、JVM 优化

1.7.2.1、JEP351: 增强 ZGC 释放未使用内存

1.7.3、新功能预览

1.7.3.1、JEP354: Switch 表达式扩展(预览功能)

1.7.3.2、JEP355: 文本块(预览功能)

1.7.4、结束语

 1.8、Java 14 新特性概述

1.8.1、语言特性增强

1.8.1.1、JEP 359: Switch 表达式(正式版)

1.8.2、新功能和库的更新

1.8.2.1、JEP 358: 改进 NullPointerExceptions 提示信息

1.8.3、旧功能的删除和弃用

1.8.3.1、JEP 367: 删除 pack200 和 unpack200 工具

1.8.4、JVM 相关

1.8.4.1、JEP 345: G1 的 NUMA 可识别内存分配

1.8.4.2、JEP 363: 删除 CMS 垃圾回收器

1.8.4.3、JEP 364&365: ZGC 支持 MacOS 和 Windows 系统(实验阶段)

1.8.4.4、JEP 366: 弃用 ParallelScavenge 和 SerialOld GC 的组合使用

1.8.5、新功能的预览和实验

1.8.5.1、JEP 305: instanceof 模式匹配(预览阶段)

1.8.5.2、JEP 359: Record 类型(预览功能)

1.8.5.3、JEP 368: 文本块(第二预览版本)

1.8.5.4、JEP 343: 打包工具(孵化器版本)

1.8.5.5、JEP 370: 外部存储器访问 API(孵化器版)

1.8.6、结束语

1.9、Java 15 新特性概述

1.9.1、语言特性增强

1.9.1.1、JEP 378: 文本块(Text Blocks)

1.9.2、新功能和库的更新

1.9.2.1、JEP 339: Edwards-Curve 数字签名算法 (EdDSA)

1.9.2.2、JEP 371: 隐藏类 Hidden Classes

1.9.2.3、JEP 373: 重新实现 DatagramSocket API

1.9.3、JVM 优化

1.9.3.1、JEP 373: ZGC: 可伸缩低延迟垃圾收集器

1.9.3.2、JEP 374: 禁用偏向锁定

1.9.3.3、JEP 379: Shenandoah:低暂停时间垃圾收集器(转正)

1.9.4、旧功能的删除和弃用

1.9.4.1、JEP 372: 移除Nashorn JavaScript引擎

1.9.4.2、JEP 381: 移除了 Solaris 和 SPARC 端口。

1.9.4.3、JEP 385: 废除 RMI 激活

1.9.5、新功能的预览和孵化

1.9.5.1、JEP 375: instanceof 自动匹配模式(第二次预览)

1.9.5.2、JEP 360: 密封的类和接口(预览)

1.9.5.3、JEP 383: 外部存储器访问 API(二次孵化器版)

1.9.5.4、JEP 384: Records (二次预览)

1.10、Java 16 新特性概述

1.10.1、语言特性增强

1.10.1.1、JEP 394: instanceof 模式匹配(正式版)

1.10.1.2、JEP 395: Records (正式版)

1.10.2、新工具和库

1.10.2.1、JEP 380:Unix-Domain 套接字通道

1.10.2.2、JEP 390: 对基于值的类发出警告

1.10.2.3、JEP 392:打包工具(正式版)

1.10.2.4、JEP 396:默认强封装 JDK 内部元素

1.10.3、JVM 优化

1.10.3.1、JEP 376:ZGC 并发线程处理

1.10.3.2、JEP 387:弹性元空间

1.10.4、新功能的预览和孵化

1.10.4.1、JEP 338:向量 API(孵化器)

1.10.4.2、JEP 389:外部链接器 API(孵化器)

1.10.4.3、JEP 393:外部存储器访问 API(第三次孵化)

1.10.4.4、JEP 397:密封类(第二预览)

1.10.5、提升 OpenJDK 开发人员的生产力

1.10.5.1、JEP 347:启用 C++14 语言特性(在 JDK 源代码中)

1.10.5.2、JEP 357:从 Mercurial 迁移到 Git & JEP 369,迁移到 GitHub

1.10.5.3、JEP 386:AlpineLinux 移植 & JEP 388:Windows/AArch64 移植

 1.11、 Java 17 新特性概述

1.11.1、语言特性增强

1.11.1.1、密封的类和接口(正式版)

1.11.2、工具库的更新

1.11.2.1、JEP 306:恢复始终严格的浮点语义

1.11.2.2、JEP 356:增强的伪随机数生成器

1.11.2.3、JDK 17之前如何生成随机数?

1.11.2.4、为什么需要增强?

1.11.2.5、增强后是什么样的?

1.11.2.6、JEP 382:新的macOS渲染管道

1.11.3、新的平台支持

1.11.3.1、JEP 391:支持macOS AArch64

1.11.4、旧功能的删除和弃用

1.11.4.1、JEP 398:弃用 Applet API

1.11.4.2、JEP 407:删除 RMI 激活

1.11.4.3、JEP 410:删除实验性 AOT 和 JIT 编译器

1.11.4.4、JEP 411:弃用安全管理器以进行删除

1.11.5、新功能的预览和孵化API

1.11.5.1、JEP 406:新增switch模式匹配(预览版)

1.11.5.2、JEP 412:外部函数和内存api (第一轮孵化)

1.11.5.3、JEP 414:Vector API(第二轮孵化)

1.11.5.4、JEP 389:外部链接器 API(孵化器)

1.11.5.5、JEP 393:外部存储器访问 API(第三次孵化)


本篇主要介绍Java8以上所有版本特性知识体系详解。

Java现在发布的版本很快,每年两个,但是真正会被大规模使用的是三年一个的TLS版本。

  • 每3年发布一个TLS,长期维护版本。意味着Java 8 ,Java 11, Java 17 才可能被大规模使用。
  • 每年发布两个正式版本,分别是3月份和9月份。

1、版本详解

1.1、Java 8 升Java 11

Java 11 在 2018 年 9 月 25 日正式发布!根据发布的规划,JDK 11 是一个长期维护的版本(LTS); 本文帮助你梳理Java 8 升Java 11 重要特性。

1.1.1、Java 8 升Java 11 重要特性必读

Java 11 在 2018 年 9 月 25 日正式发布!根据发布的规划,JDK 11 是一个长期维护的版本(LTS); 本文帮助你梳理Java 8 升Java 11 重要特性。

1.1.2、升级JDK11概述

这里帮你梳理为何JDK 11会是一个极为重要的版本以及如何去理解它。

1.1.2.1、JDK 10后版本发布规则?

Java 11 已于 2018 年 9 月 25 日正式发布,之前在 Java 10 新特性介绍 中介绍过,为了加快的版本迭代、跟进社区反馈,Java 的版本发布周期调整为每六个月一次——即每半年发布一个大版本,每个季度发布一个中间特性版本,并且做出不会跳票的承诺。通过这样的方式,Java 开发团队能够将一些重要特性尽早的合并到 Java Release 版本中,以便快速得到开发者的反馈,避免出现类似 Java 9 发布时的两次延期的情况。

按照官方介绍,新的版本发布周期将会严格按照时间节点,于每年的 3 月和 9 月发布,Java 11 发布的时间节点也正好处于 Java 8 免费更新到期的前夕。与 Java 9 和 Java 10 这两个被称为”功能性的版本”不同,Java 11 仅将提供长期支持服务(LTS, Long-Term-Support),还将作为 Java 平台的默认支持版本,并且会提供技术支持直至 2023 年 9 月,对应的补丁和安全警告等支持将持续至 2026 年。

1.1.2.2、JDK 8 升级到JDK 11 性能提升多少?

从规划调度引擎 OptaPlanner 项目对 JDK 8和 JDK 11 的性能基准测试在新窗口打开进行了对比来看:

1.1.2.3、如何更好的理解从JDK 8 到 JDK 11 升级中带来的重要特性?

主要从如下三个方面理解,后续的章节主要围绕这三个方面进行:

  1. 语言新特性
  2. 新工具和库更新
  3. JVM优化

1.1.3、语言新特性

1.1.3.1、JDK9 - 允许在接口中使用私有方法

在如下代码中,buildMessage 是接口 SayHi 中的私有方法,在默认方法 sayHi 中被使用。

  1. public interface SayHi {
  2. private String buildMessage() {
  3. return "Hello";
  4. }
  5. void sayHi(final String message);
  6. default void sayHi() {
  7. sayHi(buildMessage());
  8. }
  9. }
1.1.3.2、JDK10 - 局部变量类型推断

局部变量类型推断是 Java 10 中最值得开发人员注意的新特性,这是 Java 语言开发人员为了简化 Java 应用程序的编写而进行的又一重要改进。

这一新功能将为 Java 增加一些新语法,允许开发人员省略通常不必要的局部变量类型初始化声明。新的语法将减少 Java 代码的冗长度,同时保持对静态类型安全性的承诺。局部变量类型推断主要是向 Java 语法中引入在其他语言(比如 C#、JavaScript)中很常见的保留类型名称 var 。但需要特别注意的是: var 不是一个关键字,而是一个保留字。只要编译器可以推断此种类型,开发人员不再需要专门声明一个局部变量的类型,也就是可以随意定义变量而不必指定变量的类型。这种改进对于链式表达式来说,也会很方便。以下是一个简单的例子:

  1. var list = new ArrayList<String>(); // ArrayList<String>
  2. var stream = list.stream(); // Stream<String>

看着是不是有点 JS 的感觉?有没有感觉越来越像 JS 了?虽然变量类型的推断在 Java 中不是一个崭新的概念,但在局部变量中确是很大的一个改进。说到变量类型推断,从 Java 5 中引进泛型,到 Java 7 的 <> 操作符允许不绑定类型而初始化 List,再到 Java 8 中的 Lambda 表达式,再到现在 Java 10 中引入的局部变量类型推断,Java 类型推断正大刀阔斧地向前进步、发展。

而上面这段例子,在以前版本的 Java 语法中初始化列表的写法为:

  1. List<String> list = new ArrayList<String>();
  2. Stream<String> stream = getStream();

在运算符允许在没有绑定 ArrayList <> 的类型的情况下初始化列表的写法为:

  1. List<String> list = new LinkedList<>();
  2. Stream<String> stream = getStream();

但这种 var 变量类型推断的使用也有局限性,仅局限于具有初始化器的局部变量、增强型 for 循环中的索引变量以及在传统 for 循环中声明的局部变量,而不能用于推断方法的参数类型,不能用于构造函数参数类型推断,不能用于推断方法返回类型,也不能用于字段类型推断,同时还不能用于捕获表达式(或任何其他类型的变量声明)。

不过对于开发者而言,变量类型显式声明会提供更加全面的程序语言信息,对于理解和维护代码有很大的帮助。Java 10 中新引入的局部变量类型推断能够帮助我们快速编写更加简洁的代码,但是局部变量类型推断的保留字 var 的使用势必会引起变量类型可视化缺失,并不是任何时候使用 var 都能容易、清晰的分辨出变量的类型。一旦 var 被广泛运用,开发者在没有 IDE 的支持下阅读代码,势必会对理解程序的执行流程带来一定的困难。所以还是建议尽量显式定义变量类型,在保持代码简洁的同时,也需要兼顾程序的易读性、可维护性。

1.1.3.3、JDK11 - 用于 Lambda 参数的局部变量语法

在 Lambda 表达式中使用局部变量类型推断是 Java 11 引入的唯一与语言相关的特性,这一节,我们将探索这一新特性。

从 Java 10 开始,便引入了局部变量类型推断这一关键特性。类型推断允许使用关键字 var 作为局部变量的类型而不是实际类型,编译器根据分配给变量的值推断出类型。这一改进简化了代码编写、节省了开发者的工作时间,因为不再需要显式声明局部变量的类型,而是可以使用关键字 var,且不会使源代码过于复杂。

可以使用关键字 var 声明局部变量,如下所示:

  1. var s = "Hello Java 11";
  2. System.out.println(s);

但是在 Java 10 中,还有下面几个限制:

  • 只能用于局部变量上
  • 声明时必须初始化
  • 不能用作方法参数
  • 不能在 Lambda 表达式中使用

Java 11 与 Java 10 的不同之处在于允许开发者在 Lambda 表达式中使用 var 进行参数声明。乍一看,这一举措似乎有点多余,因为在写代码过程中可以省略 Lambda 参数的类型,并通过类型推断确定它们。但是,添加上类型定义同时使用 @Nonnull 和 @Nullable 等类型注释还是很有用的,既能保持与局部变量的一致写法,也不丢失代码简洁。

Lambda 表达式使用隐式类型定义,它形参的所有类型全部靠推断出来的。隐式类型 Lambda 表达式如下:

(x, y) -> x.process(y)

Java 10 为局部变量提供隐式定义写法如下:

  1. var x = new Foo();
  2. for (var x : xs) { ... }
  3. try (var x = ...) { ... } catch ...

为了 Lambda 类型表达式中正式参数定义的语法与局部变量定义语法的不一致,且为了保持与其他局部变量用法上的一致性,希望能够使用关键字 var 隐式定义 Lambda 表达式的形参:

(var x, var y) -> x.process(y)

于是在 Java 11 中将局部变量和 Lambda 表达式的用法进行了统一,并且可以将注释应用于局部变量和 Lambda 表达式:

  1. @Nonnull var x = new Foo();
  2. (@Nonnull var x, @Nullable var y) -> x.process(y)

1.1.4、新工具和库更新

1.1.4.1、JDK9 - 集合、Stream 和 Optional更新方法

在集合上,Java 9 增加 了 List.of()Set.of()Map.of()Map.ofEntries()等工厂方法来创建不可变集合 ,如 如下 所示。

  1. List.of();
  2. List.of("Hello", "World");
  3. List.of(1, 2, 3);
  4. Set.of();
  5. Set.of("Hello", "World");
  6. Set.of(1, 2, 3);
  7. Map.of();
  8. Map.of("Hello", 1, "World", 2);

Stream 中增加了新的方法 ofNullable、dropWhile、takeWhile 和 iterate。在 如下代码 中,流中包含了从 1 到 5 的 元素。断言检查元素是否为奇数。第一个元素 1 被删除,结果流中包含 4 个元素。

  1. @Test
  2. public void testDropWhile() throws Exception {
  3. final long count = Stream.of(1, 2, 3, 4, 5)
  4. .dropWhile(i -> i % 2 != 0)
  5. .count();
  6. assertEquals(4, count);
  7. }

Collectors 中增加了新的方法 filtering 和 flatMapping。在 如下代码 中,对于输入的 String 流 ,先通过 flatMapping 把 String 映射成 Integer 流 ,再把所有的 Integer 收集到一个集合中。

  1. @Test
  2. public void testFlatMapping() throws Exception {
  3. final Set<Integer> result = Stream.of("a", "ab", "abc")
  4. .collect(Collectors.flatMapping(v -> v.chars().boxed(),
  5. Collectors.toSet()));
  6. assertEquals(3, result.size());
  7. }

Optional 类中新增了 ifPresentOrElse、or 和 stream 等方法。在 如下代码 中,Optiona l 流中包含 3 个 元素,其中只有 2 个有值。在使用 flatMap 之后,结果流中包含了 2 个值。

  1. @Test
  2. public void testStream() throws Exception {
  3. final long count = Stream.of(
  4. Optional.of(1),
  5. Optional.empty(),
  6. Optional.of(2)
  7. ).flatMap(Optional::stream)
  8. .count();
  9. assertEquals(2, count);
  10. }
1.1.4.2、JDK9 - 进程 API (Process Handle)

Java 9 增加了 ProcessHandle 接口,可以对原生进程进行管理,尤其适合于管理长时间运行的进程。在使用 ProcessBuilder 来启动一个进程之后,可以通过 Process.toHandle()方法来得到一个 ProcessHandl e 对象的实例。通过 ProcessHandle 可以获取到由 ProcessHandle.Info 表 示的进程的基本信息,如命令行参数、可执行文件路径和启动时间等。ProcessHandle 的 onExit()方法返回一个 CompletableFuture对象,可以在进程结束时执行自定义的动作。 如下代码中给出了进程 API 的使用示例。

  1. final ProcessBuilder processBuilder = new ProcessBuilder("top")
  2. .inheritIO();
  3. final ProcessHandle processHandle = processBuilder.start().toHandle();
  4. processHandle.onExit().whenCompleteAsync((handle, throwable) -> {
  5. if (throwable == null) {
  6. System.out.println(handle.pid());
  7. } else {
  8. throwable.printStackTrace();
  9. }
  10. });
1.1.4.3、JDK9 - 变量句柄 (Var Handle)

变量句柄是一个变量或一组变量的引用,包括静态域,非静态域,数组元素和堆外数据结构中的组成部分等。变量句柄的含义类似于已有的方法句柄。变量句柄由 Java 类 java.lang.invoke.VarHandle 来表示。可以使用类 java.lang.invoke.MethodHandles.Lookup 中的静态工厂方法来创建 VarHandle 对象。通过变量句柄,可以在变量上进行各种操作。这些操作称为访问模式。不同的访问模式尤其在内存排序上的不同语义。目前一共有 31 种 访问模式,而每种访问模式都 在 VarHandle 中 有对应的方法。这些方法可以对变量进行读取、写入、原子更新、数值原子更新和比特位原子操作等。VarHandle 还可以用来访问数组中的单个元素,以及把 byte[]数组 和 ByteBuffer 当成是不同原始类型的数组来访问。

在 如下代码 中,我们创建了访问 HandleTarget 类中的域 count 的变量句柄,并在其上进行读取操作。

  1. public class HandleTarget {
  2. public int count = 1;
  3. }
  4. public class VarHandleTest {
  5. private HandleTarget handleTarget = new HandleTarget();
  6. private VarHandle varHandle;
  7. @Before
  8. public void setUp() throws Exception {
  9. this.handleTarget = new HandleTarget();
  10. this.varHandle = MethodHandles
  11. .lookup()
  12. .findVarHandle(HandleTarget.class, "count", int.class);
  13. }
  14. @Test
  15. public void testGet() throws Exception {
  16. assertEquals(1, this.varHandle.get(this.handleTarget));
  17. assertEquals(1, this.varHandle.getVolatile(this.handleTarget));
  18. assertEquals(1, this.varHandle.getOpaque(this.handleTarget));
  19. assertEquals(1, this.varHandle.getAcquire(this.handleTarget));
  20. }
  21. }
1.1.4.4、JDK9 - I/O 流新特性

类 java.io.InputStream 中增加了新的方法来读取和复制 InputStream 中包含的数据。

  • readAllBytes:读取 InputStream 中的所有剩余字节。
  • readNBytes: 从 InputStream 中读取指定数量的字节到数组中。
  • transferTo:读取 InputStream 中的全部字节并写入到指定的 OutputStream 中 。
  1. public class TestInputStream {
  2. private InputStream inputStream;
  3. private static final String CONTENT = "Hello World";
  4. @Before
  5. public void setUp() throws Exception {
  6. this.inputStream =
  7. TestInputStream.class.getResourceAsStream("/input.txt");
  8. }
  9. @Test
  10. public void testReadAllBytes() throws Exception {
  11. final String content = new String(this.inputStream.readAllBytes());
  12. assertEquals(CONTENT, content);
  13. }
  14. @Test
  15. public void testReadNBytes() throws Exception {
  16. final byte[] data = new byte[5];
  17. this.inputStream.readNBytes(data, 0, 5);
  18. assertEquals("Hello", new String(data));
  19. }
  20. @Test
  21. public void testTransferTo() throws Exception {
  22. final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
  23. this.inputStream.transferTo(outputStream);
  24. assertEquals(CONTENT, outputStream.toString());
  25. }
  26. }

ObjectInputFilter 可以对 ObjectInputStream 中 包含的内容进行检查,来确保其中包含的数据是合法的。可以使用 ObjectInputStream 的方法 setObjectInputFilter 来设置。ObjectInputFilter 在 进行检查时,可以检查如对象图的最大深度、对象引用的最大数量、输入流中的最大字节数和数组的最大长度等限制,也可以对包含的类的名称进行限制。

1.1.4.5、JDK9 - 改进应用安全性能

Java 9 新增了 4 个 SHA-3 哈希算法,SHA3-224、SHA3-256、SHA3-384 和 SHA3-512。另外也增加了通过 java.security.SecureRandom 生成使用 DRBG 算法的强随机数。如下代码中给出了 SHA-3 哈希算法的使用示例。

  1. import org.apache.commons.codec.binary.Hex;
  2. public class SHA3 {
  3. public static void main(final String[] args) throws NoSuchAlgorithmException {
  4. final MessageDigest instance = MessageDigest.getInstance("SHA3-224");
  5. final byte[] digest = instance.digest("".getBytes());
  6. System.out.println(Hex.encodeHexString(digest));
  7. }
  8. }
1.1.4.6、JDK10 - 根证书认证

自 Java 9 起在 keytool 中加入参数 -cacerts ,可以查看当前 JDK 管理的根证书。而 Java 9 中 cacerts 目录为空,这样就会给开发者带来很多不便。从 Java 10 开始,将会在 JDK 中提供一套默认的 CA 根证书

作为 JDK 一部分的 cacerts 密钥库旨在包含一组能够用于在各种安全协议的证书链中建立信任的根证书。但是,JDK 源代码中的 cacerts 密钥库至目前为止一直是空的。因此,在 JDK 构建中,默认情况下,关键安全组件(如 TLS)是不起作用的。要解决此问题,用户必须使用一组根证书配置和 cacerts 密钥库下的 CA 根证书。

1.1.4.7、JDK11 - 标准 HTTP Client 升级

Java 11 对 Java 9 中引入并在 Java 10 中进行了更新的 Http Client API 进行了标准化,在前两个版本中进行孵化的同时,Http Client 几乎被完全重写,并且现在完全支持异步非阻塞。

新版 Java 中,Http Client 的包名由 jdk.incubator.http 改为 java.net.http,该 API 通过 CompleteableFutures 提供非阻塞请求和响应语义,可以联合使用以触发相应的动作,并且 RX Flo w 的概念也在 Java 11 中得到了实现。现在,在用户层请求发布者和响应发布者与底层套接字之间追踪数据流更容易了。这降低了复杂性,并最大程度上提高了 HTTP/1 和 HTTP/2 之间的重用的可能性。

Java 11 中的新 Http Client API,提供了对 HTTP/2 等业界前沿标准的支持,同时也向下兼容 HTTP/1.1,精简而又友好的 API 接口,与主流开源 API(如:Apache HttpClient、Jetty、OkHttp 等)类似甚至拥有更高的性能。与此同时它是 Java 在 Reactive-Stream 方面的第一个生产实践,其中广泛使用了 Java Flow API,终于让 Java 标准 HTTP 类库在扩展能力等方面,满足了现代互联网的需求,是一个难得的现代 Http/2 Client API 标准的实现,Java 工程师终于可以摆脱老旧的 HttpURLConnection 了。下面模拟 Http GET 请求并打印返回内容:

  1. HttpClient client = HttpClient.newHttpClient();
  2. HttpRequest request = HttpRequest.newBuilder()
  3. .uri(URI.create("http://openjdk.java.net/"))
  4. .build();
  5. client.sendAsync(request, BodyHandlers.ofString())
  6. .thenApply(HttpResponse::body)
  7. .thenAccept(System.out::println)
  8. .join();
1.1.4.8、JDK11 - 简化启动单个源代码文件的方法

Java 11 版本中最令人兴奋的功能之一是增强 Java 启动器,使之能够运行单一文件的 Java 源代码。此功能允许使用 Java 解释器直接执行 Java 源代码。源代码在内存中编译,然后由解释器执行。唯一的约束在于所有相关的类必须定义在同一个 Java 文件中。

此功能对于开始学习 Java 并希望尝试简单程序的人特别有用,并且能与 jshell 一起使用,将成为任何初学者学习语言的一个很好的工具集。不仅初学者会受益,专业人员还可以利用这些工具来探索新的语言更改或尝试未知的 API。

如今单文件程序在编写小实用程序时很常见,特别是脚本语言领域。从中开发者可以省去用 Java 编译程序等不必要工作,以及减少新手的入门障碍。在基于 Java 10 的程序实现中可以通过三种方式启动:

  • 作为 * .class 文件
  • 作为 * .jar 文件中的主类
  • 作为模块中的主类

而在最新的 Java 11 中新增了一个启动方式,即可以在源代码中声明类,例如:如果名为 HelloWorld.java 的文件包含一个名为 hello.World 的类,那么该命令:

$ java HelloWorld.java

也等同于:

  1. $ javac HelloWorld.java
  2. $ java -cp . hello.World
1.1.4.9、JDK11 - 支持 TLS 1.3 协议

Java 11 中包含了传输层安全性(TLS)1.3 规范(RFC 8446)的实现,替换了之前版本中包含的 TLS,包括 TLS 1.2,同时还改进了其他 TLS 功能,例如 OCSP 装订扩展(RFC 6066,RFC 6961),以及会话散列和扩展主密钥扩展(RFC 7627),在安全性和性能方面也做了很多提升。

新版本中包含了 Java 安全套接字扩展(JSSE)提供 SSL,TLS 和 DTLS 协议的框架和 Java 实现。目前,JSSE API 和 JDK 实现支持 SSL 3.0,TLS 1.0,TLS 1.1,TLS 1.2,DTLS 1.0 和 DTLS 1.2。

同时 Java 11 版本中实现的 TLS 1.3,重新定义了以下新标准算法名称:

  • TLS 协议版本名称:TLSv1.3
  • SSLContext 算法名称:TLSv1.3
  • TLS 1.3 的 TLS 密码套件名称:TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384
  • 用于 X509KeyManager 的 keyType:RSASSA-PSS
  • 用于 X509TrustManager 的 authType:RSASSA-PSS

还为 TLS 1.3 添加了一个新的安全属性 jdk.tls.keyLimits。当处理了特定算法的指定数据量时,触发握手后,密钥和 IV 更新以导出新密钥。还添加了一个新的系统属性 jdk.tls.server.protocols,用于在 SunJSSE 提供程序的服务器端配置默认启用的协议套件。

之前版本中使用的 KRB5 密码套件实现已从 Java 11 中删除,因为该算法已不再安全。同时注意,TLS 1.3 与以前的版本不直接兼容。

升级到 TLS 1.3 之前,需要考虑如下几个兼容性问题:

  • TLS 1.3 使用半关闭策略,而 TLS 1.2 以及之前版本使用双工关闭策略,对于依赖于双工关闭策略的应用程序,升级到 TLS 1.3 时可能存在兼容性问题。
  • TLS 1.3 使用预定义的签名算法进行证书身份验证,但实际场景中应用程序可能会使用不被支持的签名算法。
  • TLS 1.3 再支持 DSA 签名算法,如果在服务器端配置为仅使用 DSA 证书,则无法升级到 TLS 1.3。
  • TLS 1.3 支持的加密套件与 TLS 1.2 和早期版本不同,若应用程序硬编码了加密算法单元,则在升级的过程中需要修改相应代码才能升级使用 TLS 1.3。
  • TLS 1.3 版本的 session 用行为及秘钥更新行为与 1.2 及之前的版本不同,若应用依赖于 TLS 协议的握手过程细节,则需要注意。

1.2、Java 11 升Java 17

JDK 17 在 2021 年 9 月 14 号正式发布了!根据发布的规划,这次发布的 JDK 17 是一个长期维护的版本(LTS)。SpingFramework 6 和SpringBoot 3中默认将使用JDK 17,所以JDK 17必将是使用较广泛的版本; 而从上个LTS版本JDK11到JDK17有哪些重要特性需要掌握呢?本文帮助你梳理Java 11 升Java 17 重要特性。

1.2.1、Java 11 升Java 17 重要特性必读

JDK 17 在 2021 年 9 月 14 号正式发布了!根据发布的规划,这次发布的 JDK 17 是一个长期维护的版本(LTS)。SpingFramework 6 和SpringBoot 3中默认将使用JDK 17,所以JDK 17必将是使用较广泛的版本; 而从上个LTS版本JDK11到JDK17有哪些重要特性需要掌握呢?本文帮助你梳理Java 11 升Java 17 重要特性

1.2.2、升级JDK17概述 

这里帮你梳理为何JDK 17将会是一个极为重要的版本以及如何去理解它

1.2.2.1、JDK 17升级的必要性?
  1. JDK 11 作为一个 LTS版本,它的商业支持时间框架比 JDK 8 短,JDK 11 的 LTS 会提供技术支持直至 2023 年 9 月, 对应的补丁和安全警告等支持将持续至 2026 年。JDK 17 作为下一代 LTS 将提供至少到 2026 年的支持时间框架;
  2. Java系最为重要的开发框架Spring Framework 6 和 Spring Boot 3对JDK版本的最低要求是JDK 17;所以可以预见, 为了使用Spring最新框架,很多团队和开发者将被迫从Java 11(甚至Java 8)直接升级到Java 17版本。
1.2.2.2、JDK 11 升级到JDK 17 性能提升多少?

从规划调度引擎 OptaPlanner 项目(原文在这里在新窗口打开)对 JDK 17和 JDK 11 的性能基准测试进行了对比来看:

  1. 对于 G1GC(默认),Java 17 比 Java 11 快 8.66%;
  2. 对于 ParallelGC,Java 17 比 Java 11 快 6.54%;
  3. Parallel GC 整体比 G1 GC 快 16.39%

简而言之,JDK17 更快,高吞吐量垃圾回收器比低延迟垃圾回收器更快。

1.2.2.3、如何更好的理解从JDK 11 到 JDK 17 升级中带来的重要特性?

主要从如下三个方面理解,后续的章节主要围绕这三个方面进行:

  1. 语言新特性
  2. 新工具和库更新
  3. JVM优化

1.2.3、语言新特性

1.2.3.1、JDK14 - Switch 表达式(JDK 12,13预览,14正式)

switch 表达式在之前的 Java 12 和 Java 13 中都是处于预览阶段,而在这次更新的 Java 14 中,终于成为稳定版本,能够正式可用。

switch 表达式带来的不仅仅是编码上的简洁、流畅,也精简了 switch 语句的使用方式,同时也兼容之前的 switch 语句的使用;之前使用 switch 语句时,在每个分支结束之前,往往都需要加上 break 关键字进行分支跳出,以防 switch 语句一直往后执行到整个 switch 语句结束,由此造成一些意想不到的问题。switch 语句一般使用冒号 :来作为语句分支代码的开始,而 switch 表达式则提供了新的分支切换方式,即 -> 符号右则表达式方法体在执行完分支方法之后,自动结束 switch 分支,同时 -> 右则方法块中可以是表达式、代码块或者是手动抛出的异常。以往的 switch 语句写法如下:

  1. int dayOfWeek;
  2. switch (day) {
  3. case MONDAY:
  4. case FRIDAY:
  5. case SUNDAY:
  6. dayOfWeek = 6;
  7. break;
  8. case TUESDAY:
  9. dayOfWeek = 7;
  10. break;
  11. case THURSDAY:
  12. case SATURDAY:
  13. dayOfWeek = 8;
  14. break;
  15. case WEDNESDAY:
  16. dayOfWeek = 9;
  17. break;
  18. default:
  19. dayOfWeek = 0;
  20. break;
  21. }

而现在 Java 14 可以使用 switch 表达式正式版之后,上面语句可以转换为下列写法:

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

很明显,switch 表达式将之前 switch 语句从编码方式上简化了不少,但是还是需要注意下面几点:

  • 需要保持与之前 switch 语句同样的 case 分支情况。
  • 之前需要用变量来接收返回值,而现在直接使用 yield 关键字来返回 case 分支需要返回的结果。
  • 现在的 switch 表达式中不再需要显式地使用 return、break 或者 continue 来跳出当前分支。
  • 现在不需要像之前一样,在每个分支结束之前加上 break 关键字来结束当前分支,如果不加,则会默认往后执行,直到遇到 break 关键字或者整个 switch 语句结束,在 Java 14 表达式中,表达式默认执行完之后自动跳出,不会继续往后执行。
  • 对于多个相同的 case 方法块,可以将 case 条件并列,而不需要像之前一样,通过每个 case 后面故意不加 break 关键字来使用相同方法块。

使用 switch 表达式来替换之前的 switch 语句,确实精简了不少代码,提高了编码效率,同时也可以规避一些可能由于不太经意而出现的意想不到的情况,可见 Java 在提高使用者编码效率、编码体验和简化使用方面一直在不停的努力中,同时也期待未来有更多的类似 lambda、switch 表达式这样的新特性出来。

1.2.3.2、JDK15 - 文本块(JDK 13,14预览,15正式)

文本块,是一个多行字符串,它可以避免使用大多数转义符号,自动以可预测的方式格式化字符串,并让开发人员在需要时可以控制格式。

Text Blocks首次是在JDK 13中以预览功能出现的,然后在JDK 14中又预览了一次,终于在JDK 15中被确定下来,可放心使用了。

  1. public static void main(String[] args) {
  2. String query = """
  3. SELECT * from USER \
  4. WHERE `id` = 1 \
  5. ORDER BY `id`, `name`;\
  6. """;
  7. System.out.println(query);
  8. }

运行程序,输出(可以看到展示为一行了):

SELECT * from USER WHERE `id` = 1 ORDER BY `id`, `name`;
1.2.3.3、JDK16 - instanceof 模式匹配(JDK 14,15预览,16正式)

模式匹配(Pattern Matching)最早在 Java 14 中作为预览特性引入,在 Java 15 中还是预览特性,在Java 16中成为正式版。模式匹配通过对 instacneof 运算符进行模式匹配来增强 Java 编程语言。

对 instanceof 的改进,主要目的是为了让创建对象更简单、简洁和高效,并且可读性更强、提高安全性。

在以往实际使用中,instanceof 主要用来检查对象的类型,然后根据类型对目标对象进行类型转换,之后进行不同的处理、实现不同的逻辑,具体可以参考如下:

  1. if (person instanceof Student) {
  2. Student student = (Student) person;
  3. student.say();
  4. // other student operations
  5. } else if (person instanceof Teacher) {
  6. Teacher teacher = (Teacher) person;
  7. teacher.say();
  8. // other teacher operations
  9. }

上述代码中,我们首先需要对 person 对象进行类型判断,判断 person 具体是 Student 还是 Teacher,因为这两种角色对应不同操作,亦即对应到的实际逻辑实现,判断完 person 类型之后,然后强制对 person 进行类型转换为局部变量,以方便后续执行属于该角色的特定操作。

上面这种写法,有下面两个问题:

  • 每次在检查类型之后,都需要强制进行类型转换。
  • 类型转换后,需要提前创建一个局部变量来接收转换后的结果,代码显得多余且繁琐。

对 instanceof 进行模式匹配改进之后,上面示例代码可以改写成:

  1. if (person instanceof Student student) {
  2. student.say();
  3. // other student operations
  4. } else if (person instanceof Teacher teacher) {
  5. teacher.say();
  6. // other teacher operations
  7. }

首先在 if 代码块中,对 person 对象进行类型匹配,校验 person 对象是否为 Student 类型,如果类型匹配成功,则会转换为 Student 类型,并赋值给模式局部变量 student,并且只有当模式匹配表达式匹配成功是才会生效和复制,同时这里的 student 变量只能在 if 块中使用,而不能在 else if/else 中使用,否则会报编译错误。

注意,如果 if 条件中有 && 运算符时,当 instanceof 类型匹配成功,模式局部变量的作用范围也可以相应延长,如下面代码:

if (obj instanceof String s && s.length() > 5) {.. s.contains(..) ..}

另外,需要注意,这种作用范围延长,并不适用于或 || 运算符,因为即便 || 运算符左边的 instanceof 类型匹配没有成功也不会造成短路,依旧会执行到||运算符右边的表达式,但是此时,因为 instanceof 类型匹配没有成功,局部变量并未定义赋值,此时使用会产生问题。

与传统写法对比,可以发现模式匹配不但提高了程序的安全性、健壮性,另一方面,不需要显式的去进行二次类型转换,减少了大量不必要的强制类型转换。模式匹配变量在模式匹配成功之后,可以直接使用,同时它还被限制了作用范围,大大提高了程序的简洁性、可读性和安全性。instanceof 的模式匹配,为 Java 带来的有一次便捷的提升,能够剔除一些冗余的代码,写出更加简洁安全的代码,提高码代码效率。

1.2.3.4、JDK16 - Records类型(JDK 14,15预览,16正式)

Records 最早在 Java 14 中作为预览特性引入,在 Java 15 中还是预览特性,在Java 16中成为正式版。

Record 类型允许在代码中使用紧凑的语法形式来声明类,而这些类能够作为不可变数据类型的封装持有者。Record 这一特性主要用在特定领域的类上;与枚举类型一样,Record 类型是一种受限形式的类型,主要用于存储、保存数据,并且没有其它额外自定义行为的场景下。

在以往开发过程中,被当作数据载体的类对象,在正确声明定义过程中,通常需要编写大量的无实际业务、重复性质的代码,其中包括:构造函数、属性调用、访问以及 equals() 、hashCode()、toString() 等方法,因此在 Java 14 中引入了 Record 类型,其效果有些类似 Lombok 的 @Data 注解、Kotlin 中的 data class,但是又不尽完全相同,它们的共同点都是类的部分或者全部可以直接在类头中定义、描述,并且这个类只用于存储数据而已。对于 Record 类型,具体可以用下面代码来说明:

  1. public record Person(String name, int age) {
  2. public static String address;
  3. public String getName() {
  4. return name;
  5. }
  6. }

对上述代码进行编译,然后反编译之后可以看到如下结果:

  1. public final class Person extends java.lang.Record {
  2. private final java.lang.String name;
  3. private final java.lang.String age;
  4. public Person(java.lang.String name, java.lang.String age) { /* compiled code */ }
  5. public java.lang.String getName() { /* compiled code */ }
  6. public java.lang.String toString() { /* compiled code */ }
  7. public final int hashCode() { /* compiled code */ }
  8. public final boolean equals(java.lang.Object o) { /* compiled code */ }
  9. public java.lang.String name() { /* compiled code */ }
  10. public java.lang.String age() { /* compiled code */ }
  11. }

根据反编译结果,可以得出,当用 Record 来声明一个类时,该类将自动拥有下面特征:

  • 拥有一个构造方法
  • 获取成员属性值的方法:name()、age()
  • hashCode() 方法和 euqals() 方法
  • toString() 方法
  • 类对象和属性被 final 关键字修饰,不能被继承,类的示例属性也都被 final 修饰,不能再被赋值使用。
  • 还可以在 Record 声明的类中定义静态属性、方法和示例方法。注意,不能在 Record 声明的类中定义示例字段,类也不能声明为抽象类等。

可以看到,该预览特性提供了一种更为紧凑的语法来声明类,并且可以大幅减少定义类似数据类型时所需的重复性代码。

另外 Java 14 中为了引入 Record 这种新的类型,在 java.lang.Class 中引入了下面两个新方法:

  1. RecordComponent[] getRecordComponents()
  2. boolean isRecord()

其中 getRecordComponents() 方法返回一组 java.lang.reflect.RecordComponent 对象组成的数组,java.lang.reflect.RecordComponent也是一个新引入类,该数组的元素与 Record 类中的组件相对应,其顺序与在记录声明中出现的顺序相同,可以从该数组中的每个 RecordComponent 中提取到组件信息,包括其名称、类型、泛型类型、注释及其访问方法。

而 isRecord() 方法,则返回所在类是否是 Record 类型,如果是,则返回 true。

1.2.3.5、JDK17 - 密封的类和接口(JDK 15,16预览,17正式)

封闭类可以是封闭类和或者封闭接口,用来增强 Java 编程语言,防止其他类或接口扩展或实现它们。这个特性由Java 15的预览版本晋升为正式版本。

  • 密封的类和接口解释和应用

因为我们引入了sealed classinterfaces,这些class或者interfaces只允许被指定的类或者interface进行扩展和实现。

使用修饰符sealed,您可以将一个类声明为密封类。密封的类使用reserved关键字permits列出可以直接扩展它的类。子类可以是最终的,非密封的或密封的。

之前我们的代码是这样的。

  1. public class Person { } //人
  2. class Teacher extends Person { }//教师
  3. class Worker extends Person { } //工人
  4. class Student extends Person{ } //学生

但是我们现在要限制 Person类 只能被这三个类继承,不能被其他类继承,需要这么做。

  1. // 添加sealed修饰符,permits后面跟上只能被继承的子类名称
  2. public sealed class Person permits Teacher, Worker, Student{ } //人
  3. // 子类可以被修饰为 final
  4. final class Teacher extends Person { }//教师
  5. // 子类可以被修饰为 non-sealed,此时 Worker类就成了普通类,谁都可以继承它
  6. non-sealed class Worker extends Person { } //工人
  7. // 任何类都可以继承Worker
  8. class AnyClass extends Worker{}
  9. //子类可以被修饰为 sealed,同上
  10. sealed class Student extends Person permits MiddleSchoolStudent,GraduateStudent{ } //学生
  11. final class MiddleSchoolStudent extends Student { } //中学生
  12. final class GraduateStudent extends Student { } //研究生

很强很实用的一个特性,可以限制类的层次结构。

1.2.4、新工具和库更新

1.2.4.1、JDK13 - Socket API 重构

Java 中的 Socket API 已经存在了二十多年了,尽管这么多年来,一直在维护和更新中,但是在实际使用中遇到一些局限性,并且不容易维护和调试,所以要对其进行大修大改,才能跟得上现代技术的发展,毕竟二十多年来,技术都发生了深刻的变化。Java 13 为 Socket API 带来了新的底层实现方法,并且在 Java 13 中是默认使用新的 Socket 实现,使其易于发现并在排除问题同时增加可维护性。

Java Socket API(java.net.ServerSocket 和 java.net.Socket)包含允许监听控制服务器和发送数据的套接字对象。可以使用 ServerSocket 来监听连接请求的端口,一旦连接成功就返回一个 Socket 对象,可以使用该对象读取发送的数据和进行数据写回操作,而这些类的繁重工作都是依赖于 SocketImpl 的内部实现,服务器的发送和接收两端都基于 SOCKS 进行实现的。

在 Java 13 之前,通过使用 PlainSocketImpl 作为 SocketImpl 的具体实现。

Java 13 中的新底层实现,引入 NioSocketImpl 的实现用以替换 SocketImpl 的 PlainSocketImpl 实现,此实现与 NIO(新 I/O)实现共享相同的内部基础结构,并且与现有的缓冲区高速缓存机制集成在一起,因此不需要使用线程堆栈。除了这些更改之外,还有其他一些更便利的更改,如使用 java.lang.ref.Cleaner 机制来关闭套接字(如果 SocketImpl 实现在尚未关闭的套接字上被进行了垃圾收集),以及在轮询时套接字处于非阻塞模式时处理超时操作等方面。

为了最小化在重新实现已使用二十多年的方法时出现问题的风险,在引入新实现方法的同时,之前版本的实现还未被移除,可以通过使用下列系统属性以重新使用原实现方法:

-Djdk.net.usePlainSocketImpl = true

另外需要注意的是,SocketImpl 是一种传统的 SPI 机制,同时也是一个抽象类,并未指定具体的实现,所以,新的实现方式尝试模拟未指定的行为,以达到与原有实现兼容的目的。但是,在使用新实现时,有些基本情况可能会失败,使用上述系统属性可以纠正遇到的问题,下面两个除外。

  • 老版本中,PlainSocketImpl 中的 getInputStream() 和 getOutputStream() 方法返回的 InputStream 和 OutputStream 分别来自于其对应的扩展类型 FileInputStream 和 FileOutputStream,而这个在新版实现中则没有。
  • 使用自定义或其它平台的 SocketImpl 的服务器套接字无法接受使用其他(自定义或其它平台)类型 SocketImpl 返回 Sockets 的连接。

通过这些更改,Java Socket API 将更易于维护,更好地维护将使套接字代码的可靠性得到改善。同时 NIO 实现也可以在基础层面完成,从而保持 Socket 和 ServerSocket 类层面上的不变。

1.2.4.2、JDK14 - 改进 NullPointerExceptions 提示信息

Java 14 改进 NullPointerException 的可查性、可读性,能更准确地定位 null 变量的信息。该特性能够帮助开发者和技术支持人员提高生产力,以及改进各种开发工具和调试工具的质量,能够更加准确、清楚地根据动态异常与程序代码相结合来理解程序。

相信每位开发者在实际编码过程中都遇到过 NullPointerException,每当遇到这种异常的时候,都需要根据打印出来的详细信息来分析、定位出现问题的原因,以在程序代码中规避或解决。例如,假设下面代码出现了一个 NullPointerException:

book.id = 99;

打印出来的 NullPointerException 信息如下:

  1. Exception in thread "main" java.lang.NullPointerException
  2. at Book.main(Book.java:5)

像上面这种异常,因为代码比较简单,并且异常信息中也打印出来了行号信息,开发者可以很快速定位到出现异常位置:book 为空而导致的 NullPointerException,而对于一些复杂或者嵌套的情况下出现 NullPointerException 时,仅根据打印出来的信息,很难判断实际出现问题的位置,具体见下面示例:

shoopingcart.buy.book.id = 99;

对于这种比较复杂的情况下,仅仅单根据异常信息中打印的行号,则比较难判断出现 NullPointerException 的原因。

而 Java 14 中,则做了对 NullPointerException 打印异常信息的改进增强,通过分析程序的字节码信息,能够做到准确的定位到出现 NullPointerException 的变量,并且根据实际源代码打印出详细异常信息,对于上述示例,打印信息如下:

  1. Exception in thread "main" java.lang.NullPointerException:
  2. Cannot assign field "book" because "shoopingcart.buy" is null
  3. at Book.main(Book.java:5)

对比可以看出,改进之后的 NullPointerException 信息,能够准确打印出具体哪个变量导致的 NullPointerException,减少了由于仅带行号的异常提示信息带来的困惑。该改进功能可以通过如下参数开启:

-XX:+ShowCodeDetailsInExceptionMessages

该增强改进特性,不仅适用于属性访问,还适用于方法调用、数组访问和赋值等有可能会导致 NullPointerException 的地方。

1.2.4.3、JDK15 - 隐藏类 Hidden Classes

隐藏类是为框架(frameworks)所设计的,隐藏类不能直接被其他类的字节码使用,只能在运行时生成类并通过反射间接使用它们。

该提案通过启用标准 API 来定义 无法发现 且 具有有限生命周期 的隐藏类,从而提高 JVM 上所有语言的效率。JDK内部和外部的框架将能够动态生成类,而这些类可以定义隐藏类。通常来说基于JVM的很多语言都有动态生成类的机制,这样可以提高语言的灵活性和效率。

  • 隐藏类天生为框架设计的,在运行时生成内部的class。
  • 隐藏类只能通过反射访问,不能直接被其他类的字节码访问。
  • 隐藏类可以独立于其他类加载、卸载,这可以减少框架的内存占用。

Hidden Classes是什么呢

Hidden Classes就是不能直接被其他class的二进制代码使用的class。Hidden Classes主要被一些框架用来生成运行时类,但是这些类不是被用来直接使用的,而是通过反射机制来调用。

比如在JDK8中引入的lambda表达式,JVM并不会在编译的时候将lambda表达式转换成为专门的类,而是在运行时将相应的字节码动态生成相应的类对象。

另外使用动态代理也可以为某些类生成新的动态类。

那么我们希望这些动态生成的类需要具有什么特性呢

  • 不可发现性。 因为我们是为某些静态的类动态生成的动态类,所以我们希望把这个动态生成的类看做是静态类的一部分。所以我们不希望除了该静态类之外的其他机制发现。
  • 访问控制。 我们希望在访问控制静态类的同时,也能控制到动态生成的类。
  • 生命周期。 动态生成类的生命周期一般都比较短,我们并不需要将其保存和静态类的生命周期一致。

API的支持

所以我们需要一些API来定义无法发现的且具有有限生命周期的隐藏类。这将提高所有基于JVM的语言实现的效率。

比如:

  1. java.lang.reflect.Proxy // 可以定义隐藏类作为实现代理接口的代理类。
  2. java.lang.invoke.StringConcatFactory // 可以生成隐藏类来保存常量连接方法;
  3. java.lang.invoke.LambdaMetaFactory //可以生成隐藏的nestmate类,以容纳访问封闭变量的lambda主体;

普通类是通过调用ClassLoader::defineClass创建的,而隐藏类是通过调用Lookup::defineHiddenClass创建的。这使JVM从提供的字节中派生一个隐藏类,链接该隐藏类,并返回提供对隐藏类的反射访问的查找对象。调用程序可以通过返回的查找对象来获取隐藏类的Class对象。

1.2.4.4、JDK15 - DatagramSocket API重构

重新实现了老的 DatagramSocket API 接口,更改了 java.net.DatagramSocket 和 java.net.MulticastSocket 为更加简单、现代化的底层实现,更易于维护和调试。

java.net.datagram.Socketjava.net.MulticastSocket的当前实现可以追溯到JDK 1.0,那时IPv6还在开发中。因此,当前的多播套接字实现尝试调和IPv4和IPv6难以维护的方式。

  • 通过替换 java.net.datagram 的基础实现,重新实现旧版 DatagramSocket API。
  • 更改java.net.DatagramSocketjava.net.MulticastSocket 为更加简单、现代化的底层实现。提高了 JDK 的可维护性和稳定性。
  • 通过将java.net.datagram.Socketjava.net.MulticastSocket API的底层实现替换为更简单、更现代的实现来重新实现遗留的DatagramSocket API。

新的实现

  • 易于调试和维护;
  • 与Project Loom中正在探索的虚拟线程协同。
1.2.4.5、JDK16 - 对基于值的类发出警告

JDK9注解@Deprecated得到了增强,增加了 since 和 forRemoval 两个属性,可以分别指定一个程序元素被废弃的版本,以及是否会在今后的版本中被删除。JDK16中对@jdk.internal.ValueBased注解加入了基于值的类的告警,所以继续在 Synchronized 同步块中使用值类型,将会在编译期和运行期产生警告,甚至是异常。

  • JDK9中@Deprecated增强了增加了 since 和 forRemoval 两 个属性

JDK9注解@Deprecated得到了增强,增加了 since 和 forRemoval 两个属性,可以分别指定一个程序元素被废弃的版本,以及是否会在今后的版本中被删除。

在如下的代码中,表示PdaiDeprecatedTest这个类在JDK9版本中被弃用并且在将来的某个版本中一定会被删除。

  1. @Deprecated(since="9", forRemoval = true)
  2. public class PdaiDeprecatedTest {
  3. }
  • JDK16中对基于值的类(@jdk.internal.ValueBased)给出告警

在JDK9中我们可以看到Integer.java类构造函数中加入了@Deprecated(since="9"),表示在JDK9版本中被弃用并且在将来的某个版本中一定会被删除

  1. public final class Integer extends Number implements Comparable<Integer> {
  2. // ...
  3. /**
  4. * Constructs a newly allocated {@code Integer} object that
  5. * represents the specified {@code int} value.
  6. *
  7. * @param value the value to be represented by the
  8. * {@code Integer} object.
  9. *
  10. * @deprecated
  11. * It is rarely appropriate to use this constructor. The static factory
  12. * {@link #valueOf(int)} is generally a better choice, as it is
  13. * likely to yield significantly better space and time performance.
  14. */
  15. @Deprecated(since="9")
  16. public Integer(int value) {
  17. this.value = value;
  18. }
  19. // ...
  20. }

如下是JDK16中Integer.java的代码

  1. /*
  2. * <p>This is a <a href="{@docRoot}/java.base/java/lang/doc-files/ValueBased.html">value-based</a>
  3. * class; programmers should treat instances that are
  4. * {@linkplain #equals(Object) equal} as interchangeable and should not
  5. * use instances for synchronization, or unpredictable behavior may
  6. * occur. For example, in a future release, synchronization may fail.
  7. *
  8. * <p>Implementation note: The implementations of the "bit twiddling"
  9. * methods (such as {@link #highestOneBit(int) highestOneBit} and
  10. * {@link #numberOfTrailingZeros(int) numberOfTrailingZeros}) are
  11. * based on material from Henry S. Warren, Jr.'s <i>Hacker's
  12. * Delight</i>, (Addison Wesley, 2002).
  13. *
  14. * @author Lee Boynton
  15. * @author Arthur van Hoff
  16. * @author Josh Bloch
  17. * @author Joseph D. Darcy
  18. * @since 1.0
  19. */
  20. @jdk.internal.ValueBased
  21. public final class Integer extends Number
  22. implements Comparable<Integer>, Constable, ConstantDesc {
  23. // ...
  24. /**
  25. * Constructs a newly allocated {@code Integer} object that
  26. * represents the specified {@code int} value.
  27. *
  28. * @param value the value to be represented by the
  29. * {@code Integer} object.
  30. *
  31. * @deprecated
  32. * It is rarely appropriate to use this constructor. The static factory
  33. * {@link #valueOf(int)} is generally a better choice, as it is
  34. * likely to yield significantly better space and time performance.
  35. */
  36. @Deprecated(since="9", forRemoval = true)
  37. public Integer(int value) {
  38. this.value = value;
  39. }
  40. // ...

添加@jdk.internal.ValueBased@Deprecated(since="9", forRemoval = true)的作用是什么呢?

  1. JDK设计者建议使用Integer a = 10或者Integer.valueOf()函数,而不是new Integer(),让其抛出告警?

在构造函数上都已经标记有@Deprecated(since="9", forRemoval = true)注解,这就意味着其构造函数在将来会被删除,不应该在程序中继续使用诸如new Integer(); 如果继续使用,编译期将会产生'Integer(int)' is deprecated and marked for removal 告警。

  1. 在并发环境下,Integer 对象根本无法通过 Synchronized 来保证线程安全,让其抛出告警?

由于JDK中对@jdk.internal.ValueBased注解加入了基于值的类的告警,所以继续在 Synchronized 同步块中使用值类型,将会在编译期和运行期产生警告,甚至是异常。

  1. public void inc(Integer count) {
  2. for (int i = 0; i < 10; i++) {
  3. new Thread(() -> {
  4. synchronized (count) { // 这里会产生编译告警
  5. count++;
  6. }
  7. }).start();
  8. }
  9. }
1.2.4.6、JDK17 - 增强的伪随机数生成器

为伪随机数生成器 (PRNG) 提供新的接口类型和实现。这一变化提高了不同 PRNG 的互操作性,并使得根据需求请求算法变得容易,而不是硬编码特定的实现。简单而言只需要理解如下三个问题:

1.2.4.6.1、JDK 17之前如何生成随机数
  1. Random 类

典型的使用如下,随机一个int值

  1. // random int
  2. new Random().nextInt();
  3. /**
  4. * description 获取指定位数的随机数
  5. *
  6. * @param length 1
  7. * @return java.lang.String
  8. */
  9. public static String getRandomString(int length) {
  10. String base = "abcdefghijklmnopqrstuvwxyz0123456789";
  11. Random random = new Random();
  12. StringBuilder sb = new StringBuilder();
  13. for (int i = 0; i < length; i++) {
  14. int number = random.nextInt(base.length());
  15. sb.append(base.charAt(number));
  16. }
  17. return sb.toString();
  18. }
  1. ThreadLocalRandom 类

提供线程间独立的随机序列。它只有一个实例,多个线程用到这个实例,也会在线程内部各自更新状态。它同时也是 Random 的子类,不过它几乎把所有 Random 的方法又实现了一遍。

  1. /**
  2. * nextInt(bound) returns 0 <= value < bound; repeated calls produce at
  3. * least two distinct results
  4. */
  5. public void testNextIntBounded() {
  6. // sample bound space across prime number increments
  7. for (int bound = 2; bound < MAX_INT_BOUND; bound += 524959) {
  8. int f = ThreadLocalRandom.current().nextInt(bound);
  9. assertTrue(0 <= f && f < bound);
  10. int i = 0;
  11. int j;
  12. while (i < NCALLS &&
  13. (j = ThreadLocalRandom.current().nextInt(bound)) == f) {
  14. assertTrue(0 <= j && j < bound);
  15. ++i;
  16. }
  17. assertTrue(i < NCALLS);
  18. }
  19. }
  1. SplittableRandom 类

非线程安全,但可以 fork 的随机序列实现,适用于拆分子任务的场景。

  1. /**
  2. * Repeated calls to nextLong produce at least two distinct results
  3. */
  4. public void testNextLong() {
  5. SplittableRandom sr = new SplittableRandom();
  6. long f = sr.nextLong();
  7. int i = 0;
  8. while (i < NCALLS && sr.nextLong() == f)
  9. ++i;
  10. assertTrue(i < NCALLS);
  11. }
1.2.4.6.2、为什么需要增强
  1. 上述几个类实现代码质量和接口抽象不佳
  2. 缺少常见的伪随机算法
  3. 自定义扩展随机数的算法只能自己去实现,缺少统一的接口
1.2.4.6.3、增强后是什么样的

代码的优化自不必说,我们就看下新增了哪些常见的伪随机算法

如何使用这个呢?可以使用RandomGenerator

RandomGenerator g = RandomGenerator.of("L64X128MixRandom");

1.3、Java 9 新特性详解

Java 9 正式发布于 2017 年 9 月 21 日。作为 Java8 之后 3 年半才发布的新版本,Java 9 带来了很多重大的变化。其中最重要的改动是 Java 平台模块系统的引入。除此之外,还有一些新的特性。本文对 Java9 中包含的新特性做了概括性的介绍,可以帮助你快速了解 Java 9。

知识体系系统性梳理

1.3.1、Java 9 新特性概述

Java 9 正式发布于 2017 年 9 月 21 日。作为 Java8 之后 3 年半才发布的新版本,Java 9 带来了很多重大的变化。其中最重要的改动是 Java 平台模块系统的引入。除此之外,还有一些新的特性。本文对 Java9 中包含的新特性做了概括性的介绍,可以帮助你快速了解 Java 9

1.3.2、Java 平台 模块系统

Java 平台模块系统,也就是 Project Jigsaw,把模块化开发实践引入到了 Java 平台中。在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具,创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。这对于目前流行的不可变基础设施的实践来说,镜像的大小的减少可以节省很多存储空间和带宽资源 。

模块化开发的实践在软件开发领域并不是一个新的概念。Java 开发社区已经使用这样的模块化实践有相当长的一段时间。主流的构建工具,包括 Apache Maven 和 Gradle 都支持把一个大的项目划分成若干个子项目。子项目之间通过不同的依赖关系组织在一起。每个子项目在构建之后都会产生对应的 JAR 文件。 在 Java9 中 ,已有的这些项目可以很容易的升级转换为 Java 9 模块 ,并保持原有的组织结构不变。

Java 9 模块的重要特征是在其工件(artifact)的根目录中包含了一个描述模块的 module-info.class 文 件。 工件的格式可以是传统的 JAR 文件或是 Java 9 新增的 JMOD 文件。这个文件由根目录中的源代码文件 module-info.java 编译而来。该模块声明文件可以描述模块的不同特征。模块声明文件中可以包含的内容如下:

  • 模块导出的包:使用 exports 可以声明模块对其他模块所导出的包。包中的 public 和 protected 类型,以及这些类型的 public 和 protected 成员可以被其他模块所访问。没有声明为导出的包相当于模块中的私有成员,不能被其他模块使用。
  • 模块的依赖关系:使用 requires 可以声明模块对其他模块的依赖关系。使用 requires transitive 可 以把一个模块依赖声明为传递的。传递的模块依赖可以被依赖当前模块的其他模块所读取。 如果一个模块所导出的类型的型构中包含了来自它所依赖的模块的类型,那么对该模块的依赖应该声明为传递的。
  • 服务的提供和使用:如果一个模块中包含了可以被 ServiceLocator 发现的服务接口的实现 ,需要使用 provides with 语句来声明具体的实现类 ;如果一个模块需要使用服务接口,可以使用 uses 语句来声明。

如下代码中给出了一个模块声明文件的示例。在该声明文件中,模块 com.mycompany.sample 导出了 Java 包 com.mycompany.sample。该模块依赖于模块 com.mycompany.common 。该模块也提供了服务接口 com.mycompany.common.DemoService 的实现类 com.mycompany.sample.DemoServiceImpl

  1. module com.mycompany.sample {
  2. exports com.mycompany.sample;
  3. requires com.mycompany.common;
  4. provides com.mycompany.common.DemoService with
  5. com.mycompany.sample.DemoServiceImpl;
  6. }

模块系统中增加了模块路径的概念。模块系统在解析模块时,会从模块路径中进行查找。为了保持与之前 Java 版本的兼容性,CLASSPATH 依然被保留。所有的类型在运行时都属于某个特定的模块。对于从 CLASSPATH 中加载的类型,它们属于加载它们的类加载器对应的未命名模块。可以通过 Class 的 getModule()方法来获取到表示其所在模块的 Module 对象。

在 JVM 启动时,会从应用的根模块开始,根据依赖关系递归的进行解析,直到得到一个表示依赖关系的图。如果解析过程中出现找不到模块的情况,或是在模块路径的同一个地方找到了名称相同的模块,模块解析过程会终止,JVM 也会退出。Java 也提供了相应的 API 与模块系统进行交互。

1.3.3、Jshell

jshell 是 Java 9 新增的一个实用工具。jshell 为 Java 增加了类似 NodeJS 和 Python 中的读取-求值-打印循环( Read-Evaluation-Print Loop ) 。 在 jshell 中 可以直接 输入表达式并查看其执行结果。当需要测试一个方法的运行效果,或是快速的对表达式进行求值时,jshell 都非常实用。只需要通过 jshell 命令启动 jshell,然后直接输入表达式即可。每个表达式的结果会被自动保存下来 ,以数字编号作为引用,类似 $1 和$2 这样的名称 。可以在后续的表达式中引用之前语句的运行结果。 在 jshell 中 ,除了表达式之外,还可以创建 Java 类和方法。jshell 也有基本的代码完成功能。

在 如下代码 中,我们直接创建了一个方法 add。

  1. jshell> int add(int x, int y) {
  2. ...> return x + y;
  3. ...> }
  4. | created method add(int,int)

接着就可以在 jshell 中直接使用这个方法,如下代码 所示。

  1. jshell> add(1, 2)
  2. $19 ==> 3

1.3.4、集合、Stream 和 Optional

在集合上,Java 9 增加 了 List.of()Set.of()Map.of()Map.ofEntries()等工厂方法来创建不可变集合 ,如 如下 所示。

  1. List.of();
  2. List.of("Hello", "World");
  3. List.of(1, 2, 3);
  4. Set.of();
  5. Set.of("Hello", "World");
  6. Set.of(1, 2, 3);
  7. Map.of();
  8. Map.of("Hello", 1, "World", 2);

Stream 中增加了新的方法 ofNullable、dropWhile、takeWhile 和 iterate。在 如下代码 中,流中包含了从 1 到 5 的 元素。断言检查元素是否为奇数。第一个元素 1 被删除,结果流中包含 4 个元素。

  1. @Test
  2. public void testDropWhile() throws Exception {
  3. final long count = Stream.of(1, 2, 3, 4, 5)
  4. .dropWhile(i -> i % 2 != 0)
  5. .count();
  6. assertEquals(4, count);
  7. }

Collectors 中增加了新的方法 filtering 和 flatMapping。在 如下代码 中,对于输入的 String 流 ,先通过 flatMapping 把 String 映射成 Integer 流 ,再把所有的 Integer 收集到一个集合中。

  1. @Test
  2. public void testFlatMapping() throws Exception {
  3. final Set<Integer> result = Stream.of("a", "ab", "abc")
  4. .collect(Collectors.flatMapping(v -> v.chars().boxed(),
  5. Collectors.toSet()));
  6. assertEquals(3, result.size());
  7. }

Optional 类中新增了 ifPresentOrElse、or 和 stream 等方法。在 如下代码 中,Optiona l 流中包含 3 个 元素,其中只有 2 个有值。在使用 flatMap 之后,结果流中包含了 2 个值。

  1. @Test
  2. public void testStream() throws Exception {
  3. final long count = Stream.of(
  4. Optional.of(1),
  5. Optional.empty(),
  6. Optional.of(2)
  7. ).flatMap(Optional::stream)
  8. .count();
  9. assertEquals(2, count);
  10. }

1.3.5、进程 API

Java 9 增加了 ProcessHandle 接口,可以对原生进程进行管理,尤其适合于管理长时间运行的进程。在使用 ProcessBuilder 来启动一个进程之后,可以通过 Process.toHandle()方法来得到一个 ProcessHandl e 对象的实例。通过 ProcessHandle 可以获取到由 ProcessHandle.Info 表 示的进程的基本信息,如命令行参数、可执行文件路径和启动时间等。ProcessHandle 的 onExit()方法返回一个 CompletableFuture对象,可以在进程结束时执行自定义的动作。 如下代码中给出了进程 API 的使用示例。

  1. final ProcessBuilder processBuilder = new ProcessBuilder("top")
  2. .inheritIO();
  3. final ProcessHandle processHandle = processBuilder.start().toHandle();
  4. processHandle.onExit().whenCompleteAsync((handle, throwable) -> {
  5. if (throwable == null) {
  6. System.out.println(handle.pid());
  7. } else {
  8. throwable.printStackTrace();
  9. }
  10. });

1.3.6、平台日志 API 和 服务

Java 9 允许为 JDK 和应用配置同样的日志实现。新增的 System.LoggerFinder 用来管理 JDK 使 用的日志记录器实现。JVM 在运行时只有一个系统范围的 LoggerFinder 实例。LoggerFinder 通 过服务查找机制来加载日志记录器实现。默认情况下,JDK 使用 java.logging 模块中的 java.util.logging 实现。通过 LoggerFinder 的 getLogger()方法就可以获取到表示日志记录器的 System.Logger 实现。应用同样可以使用 System.Logger 来记录日志。这样就保证了 JDK 和应用使用同样的日志实现。我们也可以通过添加自己的 System.LoggerFinder 实现来让 JDK 和应用使用 SLF4J 等其他日志记录框架。 代码清单 9 中给出了平台日志 API 的使用示例。

  1. public class Main {
  2. private static final System.Logger LOGGER = System.getLogger("Main");
  3. public static void main(final String[] args) {
  4. LOGGER.log(Level.INFO, "Run!");
  5. }
  6. }

1.3.7、反应式流 ( Reactive Streams )

反应式编程的思想最近得到了广泛的流行。 在 Java 平台上有流行的反应式 库 RxJava 和 R eactor。反应式流规范的出发点是提供一个带非阻塞负压( non-blocking backpressure ) 的异步流处理规范。反应式流规范的核心接口已经添加到了 Java9 中的 java.util.concurrent.Flow 类中。

Flow 中包含了 Flow.Publisher、Flow.Subscriber、Flow.Subscription 和 F low.Processor 等 4 个核心接口。Java 9 还提供了 SubmissionPublisher 作为 Flow.Publisher 的一个实现。RxJava 2 和 Reactor 都可以很方便的 与 Flow 类的核心接口进行互操作。

1.3.8、变量句柄

变量句柄是一个变量或一组变量的引用,包括静态域,非静态域,数组元素和堆外数据结构中的组成部分等。变量句柄的含义类似于已有的方法句柄。变量句柄由 Java 类 java.lang.invoke.VarHandle 来表示。可以使用类 java.lang.invoke.MethodHandles.Lookup 中的静态工厂方法来创建 VarHandle 对象。通过变量句柄,可以在变量上进行各种操作。这些操作称为访问模式。不同的访问模式尤其在内存排序上的不同语义。目前一共有 31 种 访问模式,而每种访问模式都 在 VarHandle 中 有对应的方法。这些方法可以对变量进行读取、写入、原子更新、数值原子更新和比特位原子操作等。VarHandle 还可以用来访问数组中的单个元素,以及把 byte[]数组 和 ByteBuffer 当成是不同原始类型的数组来访问。

在 如下代码 中,我们创建了访问 HandleTarget 类中的域 count 的变量句柄,并在其上进行读取操作。

  1. public class HandleTarget {
  2. public int count = 1;
  3. }
  4. public class VarHandleTest {
  5. private HandleTarget handleTarget = new HandleTarget();
  6. private VarHandle varHandle;
  7. @Before
  8. public void setUp() throws Exception {
  9. this.handleTarget = new HandleTarget();
  10. this.varHandle = MethodHandles
  11. .lookup()
  12. .findVarHandle(HandleTarget.class, "count", int.class);
  13. }
  14. @Test
  15. public void testGet() throws Exception {
  16. assertEquals(1, this.varHandle.get(this.handleTarget));
  17. assertEquals(1, this.varHandle.getVolatile(this.handleTarget));
  18. assertEquals(1, this.varHandle.getOpaque(this.handleTarget));
  19. assertEquals(1, this.varHandle.getAcquire(this.handleTarget));
  20. }
  21. }

1.3.9、改进方法句柄(Method Handle)

类 java.lang.invoke.MethodHandles 增加了更多的静态方法来创建不同类型的方法句柄。

  • arrayConstructor:创建指定类型的数组。
  • arrayLength:获取指定类型的数组的大小。
  • varHandleInvoker 和 varHandleExactInvoker:调用 VarHandle 中的访问模式方法。
  • zero:返回一个类型的默认值。
  • empty:返 回 MethodType 的返回值类型的默认值。
  • loop、countedLoop、iteratedLoop、whileLoop 和 doWhileLoop:创建不同类型的循环,包括 for 循环、while 循环 和 do-while 循环。
  • tryFinally:把对方法句柄的调用封装在 try-finally 语句中。

在 如下代码 中,我们使用 iteratedLoop 来创建一个遍历 S tring 类型迭代器的方法句柄,并计算所有字符串的长度的总和。

  1. public class IteratedLoopTest {
  2. static int body(final int sum, final String value) {
  3. return sum + value.length();
  4. }
  5. @Test
  6. public void testIteratedLoop() throws Throwable {
  7. final MethodHandle iterator = MethodHandles.constant(
  8. Iterator.class,
  9. List.of("a", "bc", "def").iterator());
  10. final MethodHandle init = MethodHandles.zero(int.class);
  11. final MethodHandle body = MethodHandles
  12. .lookup()
  13. .findStatic(
  14. IteratedLoopTest.class,
  15. "body",
  16. MethodType.methodType(
  17. int.class,
  18. int.class,
  19. String.class));
  20. final MethodHandle iteratedLoop = MethodHandles
  21. .iteratedLoop(iterator, init, body);
  22. assertEquals(6, iteratedLoop.invoke());
  23. }
  24. }

1.3.10、并发

在并发方面,类 CompletableFuture 中增加了几个新的方法。completeAsync 使用一个异步任务来获取结果并完成该 CompletableFuture。orTimeout 在 CompletableFuture 没有在给定的超时时间之前完成,使用 TimeoutException 异常来完成 CompletableFuture。completeOnTimeout 与 orTimeout 类似,只不过它在超时时使用给定的值来完成 CompletableFuture。新的 Thread.onSpinWait 方法在当前线程需要使用忙循环来等待时,可以提高等待的效率。

1.3.11、Nashorn

Nashorn 是 Java 8 中引入的新的 JavaScript 引擎。Java 9 中的 Nashorn 已经实现了一些 ECMAScript 6 规范中的新特性,包括模板字符串、二进制和八进制字面量、迭代器 和 for..of 循环和箭头函数等。Nashorn 还提供了 API 把 ECMAScript 源代码解析成抽象语法树( Abstract Syntax Tree,AST ) ,可以用来对 ECMAScript 源代码进行分析。

1.3.12、I/O 流新特性

类 java.io.InputStream 中增加了新的方法来读取和复制 InputStream 中包含的数据。

  • readAllBytes:读取 InputStream 中的所有剩余字节。
  • readNBytes: 从 InputStream 中读取指定数量的字节到数组中。
  • transferTo:读取 InputStream 中的全部字节并写入到指定的 OutputStream 中 。
  1. public class TestInputStream {
  2. private InputStream inputStream;
  3. private static final String CONTENT = "Hello World";
  4. @Before
  5. public void setUp() throws Exception {
  6. this.inputStream =
  7. TestInputStream.class.getResourceAsStream("/input.txt");
  8. }
  9. @Test
  10. public void testReadAllBytes() throws Exception {
  11. final String content = new String(this.inputStream.readAllBytes());
  12. assertEquals(CONTENT, content);
  13. }
  14. @Test
  15. public void testReadNBytes() throws Exception {
  16. final byte[] data = new byte[5];
  17. this.inputStream.readNBytes(data, 0, 5);
  18. assertEquals("Hello", new String(data));
  19. }
  20. @Test
  21. public void testTransferTo() throws Exception {
  22. final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
  23. this.inputStream.transferTo(outputStream);
  24. assertEquals(CONTENT, outputStream.toString());
  25. }
  26. }

ObjectInputFilter 可以对 ObjectInputStream 中 包含的内容进行检查,来确保其中包含的数据是合法的。可以使用 ObjectInputStream 的方法 setObjectInputFilter 来设置。ObjectInputFilter 在 进行检查时,可以检查如对象图的最大深度、对象引用的最大数量、输入流中的最大字节数和数组的最大长度等限制,也可以对包含的类的名称进行限制。

1.3.13、改进应用安全性能

Java 9 新增了 4 个 SHA-3 哈希算法,SHA3-224、SHA3-256、SHA3-384 和 SHA3-512。另外也增加了通过 java.security.SecureRandom 生成使用 DRBG 算法的强随机数。如下代码中给出了 SHA-3 哈希算法的使用示例。

  1. import org.apache.commons.codec.binary.Hex;
  2. public class SHA3 {
  3. public static void main(final String[] args) throws NoSuchAlgorithmException {
  4. final MessageDigest instance = MessageDigest.getInstance("SHA3-224");
  5. final byte[] digest = instance.digest("".getBytes());
  6. System.out.println(Hex.encodeHexString(digest));
  7. }
  8. }

1.3.14、用户界面

类 java.awt.Desktop 增加了新的与桌面进行互动的能力。可以使用 addAppEventListener 方法来添加不同应用事件的监听器,包括应用变为前台应用、应用隐藏或显示、屏幕和系统进入休眠与唤醒、以及 用户会话的开始和终止等。还可以在显示关于窗口和配置窗口时,添加自定义的逻辑。在用户要求退出应用时,可以通过自定义处理器来接受或拒绝退出请求。在 A WT 图像支持方面,可以在应用中使用多分辨率图像。

1.3.15、统一 JVM 日志

Java 9 中 ,JVM 有了统一的日志记录系统,可以使用新的命令行选项-Xlog 来控制 JVM 上 所有组件的日志记录。该日志记录系统可以设置输出的日志消息的标签、级别、修饰符和输出目标等。Java 9 移除了在 Java 8 中 被废弃的垃圾回收器配置组合,同时 把 G1 设为默认的垃圾回收器实现。另外,CMS 垃圾回收器已经被声明为废弃。Java 9 也增加了很多可以通过 jcmd 调用的诊断命令。

1.3.16、其他改动方面

  • 在 Java 语言本身,Java 9 允许在接口中使用私有方法。

在如下代码中,buildMessage 是接口 SayHi 中的私有方法,在默认方法 sayHi 中被使用。

  1. public interface SayHi {
  2. private String buildMessage() {
  3. return "Hello";
  4. }
  5. void sayHi(final String message);
  6. default void sayHi() {
  7. sayHi(buildMessage());
  8. }
  9. }
  • 在 try-with-resources 语句中可以使用 effectively-final 变量。
  • 类 java.lang.StackWalker 可以对线程的堆栈进行遍历,并且支持过滤和延迟访问。Java 9 把对 Unicode 的支持升级到了 8.0。
  • ResourceBundle 加载属性文件的默认编码从 ISO-8859-1 改成了 UTF-8,不再需要使用 native2ascii 命 令来对属性文件进行额外处理。
  • 注解@Deprecated 也得到了增强,增加了 since 和 forRemoval 两 个属性,可以分别指定一个程序元素被废弃的版本,以及是否会在今后的版本中被删除。

在如下的代码中,表示PdaiDeprecatedTest这个类在JDK9版本中被弃用并且在将来的某个版本中一定会被删除。

  1. @Deprecated(since="9", forRemoval = true)
  2. public class PdaiDeprecatedTest {
  3. }

1.3.17、结束语

作为 Java 平台最新的一个重大更新,Java 9 中的很多新特性,尤其模块系统,对于 Java 应用的开发会产生深远的影响。本文对 Java 9 中的新特性做了概括的介绍,可以作为了解 Java 9 的基础。这些新特性的相信内容,可以通过官方文档来进一步的了解。

1.4、Java 10 新特性概述

作为当今使用最广泛的编程语言之一的 Java 在 2018 年 3 月 21 日发布了第十个大版本。为了更快地迭代、更好地跟进社区反馈,Java 语言版本发布周期调整为每隔 6 个月发布一次。Java 10 是这一新规则之后,采用新发布周期的第一个大版本。Java 10 版本带来了很多新特性,其中最备受广大开发者关注的莫过于局部变量类型推断。除此之外,还有其他包括垃圾收集器改善、GC 改进、性能提升、线程管控等一批新特性。本文主要针对 Java 10 中的新特性展开介绍,希望读者能从本文的介绍中快速了解 Java 10 带来的变化。

知识体系系统性梳理

1.4.1、Java 10 新特性概述 

作为当今使用最广泛的编程语言之一的 Java 在 2018 年 3 月 21 日发布了第十个大版本。为了更快地迭代、更好地跟进社区反馈,Java 语言版本发布周期调整为每隔 6 个月发布一次。Java 10 是这一新规则之后,采用新发布周期的第一个大版本。Java 10 版本带来了很多新特性,其中最备受广大开发者关注的莫过于局部变量类型推断。除此之外,还有其他包括垃圾收集器改善、GC 改进、性能提升、线程管控等一批新特性。本文主要针对 Java 10 中的新特性展开介绍,希望读者能从本文的介绍中快速了解 Java 10 带来的变化。

1.4.2、局部变量类型推断

局部变量类型推断是 Java 10 中最值得开发人员注意的新特性,这是 Java 语言开发人员为了简化 Java 应用程序的编写而进行的又一重要改进。

这一新功能将为 Java 增加一些新语法,允许开发人员省略通常不必要的局部变量类型初始化声明。新的语法将减少 Java 代码的冗长度,同时保持对静态类型安全性的承诺。局部变量类型推断主要是向 Java 语法中引入在其他语言(比如 C#、JavaScript)中很常见的保留类型名称 var 。但需要特别注意的是: var 不是一个关键字,而是一个保留字。只要编译器可以推断此种类型,开发人员不再需要专门声明一个局部变量的类型,也就是可以随意定义变量而不必指定变量的类型。这种改进对于链式表达式来说,也会很方便。以下是一个简单的例子:

清单 1. 局部变量类型推断示例

  1. var list = new ArrayList<String>(); // ArrayList<String>
  2. var stream = list.stream(); // Stream<String>

看着是不是有点 JS 的感觉?有没有感觉越来越像 JS 了?虽然变量类型的推断在 Java 中不是一个崭新的概念,但在局部变量中确是很大的一个改进。说到变量类型推断,从 Java 5 中引进泛型,到 Java 7 的 <> 操作符允许不绑定类型而初始化 List,再到 Java 8 中的 Lambda 表达式,再到现在 Java 10 中引入的局部变量类型推断,Java 类型推断正大刀阔斧地向前进步、发展。

而上面这段例子,在以前版本的 Java 语法中初始化列表的写法为:

清单 2. Java 类型初始化示例

  1. List<String> list = new ArrayList<String>();
  2. Stream<String> stream = getStream();

在运算符允许在没有绑定 ArrayList <> 的类型的情况下初始化列表的写法为:

清单 3. Java 7 之后版本类型初始化示例

  1. List<String> list = new LinkedList<>();
  2. Stream<String> stream = getStream();

但这种 var 变量类型推断的使用也有局限性,仅局限于具有初始化器的局部变量、增强型 for 循环中的索引变量以及在传统 for 循环中声明的局部变量,而不能用于推断方法的参数类型,不能用于构造函数参数类型推断,不能用于推断方法返回类型,也不能用于字段类型推断,同时还不能用于捕获表达式(或任何其他类型的变量声明)。

不过对于开发者而言,变量类型显式声明会提供更加全面的程序语言信息,对于理解和维护代码有很大的帮助。Java 10 中新引入的局部变量类型推断能够帮助我们快速编写更加简洁的代码,但是局部变量类型推断的保留字 var 的使用势必会引起变量类型可视化缺失,并不是任何时候使用 var 都能容易、清晰的分辨出变量的类型。一旦 var 被广泛运用,开发者在没有 IDE 的支持下阅读代码,势必会对理解程序的执行流程带来一定的困难。所以还是建议尽量显式定义变量类型,在保持代码简洁的同时,也需要兼顾程序的易读性、可维护性。

1.4.3、整合 JDK 代码仓库

为了简化开发流程,Java 10 中会将多个代码库合并到一个代码仓库中。

在已发布的 Java 版本中,JDK 的整套代码根据不同功能已被分别存储在多个 Mercurial 存储库,这八个 Mercurial 存储库分别是:root、corba、hotspot、jaxp、jaxws、jdk、langtools、nashorn。

虽然以上八个存储库之间相互独立以保持各组件代码清晰分离,但同时管理这些存储库存在许多缺点,并且无法进行相关联源代码的管理操作。其中最重要的一点是,涉及多个存储库的变更集无法进行原子提交 (atomic commit)。例如,如果一个 bug 修复时需要对独立存储两个不同代码库的代码进行更改,那么必须创建两个提交:每个存储库中各一个。这种不连续性很容易降低项目和源代码管理工具的可跟踪性和加大复杂性。特别是,不可能跨越相互依赖的变更集的存储库执行原子提交这种多次跨仓库的变化是常见现象。

为了解决这个问题,JDK 10 中将所有现有存储库合并到一个 Mercurial 存储库中。这种合并的一个次生效应是,单一的 Mercurial 存储库比现有的八个存储库要更容易地被镜像(作为一个 Git 存储库),并且使得跨越相互依赖的变更集的存储库运行原子提交成为可能,从而简化开发和管理过程。虽然在整合过程中,外部开发人员有一些阻力,但是 JDK 开发团队已经使这一更改成为 JDK 10 的一部分。

1.4.4、统一的垃圾回收接口

在当前的 Java 结构中,组成垃圾回收器(GC)实现的组件分散在代码库的各个部分。尽管这些惯例对于使用 GC 计划的 JDK 开发者来说比较熟悉,但对新的开发人员来说,对于在哪里查找特定 GC 的源代码,或者实现一个新的垃圾收集器常常会感到困惑。更重要的是,随着 Java modules 的出现,我们希望在构建过程中排除不需要的 GC,但是当前 GC 接口的横向结构会给排除、定位问题带来困难。

为解决此问题,需要整合并清理 GC 接口,以便更容易地实现新的 GC,并更好地维护现有的 GC。Java 10 中,hotspot/gc 代码实现方面,引入一个干净的 GC 接口,改进不同 GC 源代码的隔离性,多个 GC 之间共享的实现细节代码应该存在于辅助类中。这种方式提供了足够的灵活性来实现全新 GC 接口,同时允许以混合搭配方式重复使用现有代码,并且能够保持代码更加干净、整洁,便于排查收集器问题。

1.4.5、并行全垃圾回收器 G1

大家如果接触过 Java 性能调优工作,应该会知道,调优的最终目标是通过参数设置来达到快速、低延时的内存垃圾回收以提高应用吞吐量,尽可能的避免因内存回收不及时而触发的完整 GC(Full GC 会带来应用出现卡顿)。

G1 垃圾回收器是 Java 9 中 Hotspot 的默认垃圾回收器,是以一种低延时的垃圾回收器来设计的,旨在避免进行 Full GC,但是当并发收集无法快速回收内存时,会触发垃圾回收器回退进行 Full GC。之前 Java 版本中的 G1 垃圾回收器执行 GC 时采用的是基于单线程标记扫描压缩算法(mark-sweep-compact)。为了最大限度地减少 Full GC 造成的应用停顿的影响,Java 10 中将为 G1 引入多线程并行 GC,同时会使用与年轻代回收和混合回收相同的并行工作线程数量,从而减少了 Full GC 的发生,以带来更好的性能提升、更大的吞吐量。

Java 10 中将采用并行化 mark-sweep-compact 算法,并使用与年轻代回收和混合回收相同数量的线程。具体并行 GC 线程数量可以通过: -XX:ParallelGCThreads 参数来调节,但这也会影响用于年轻代和混合收集的工作线程数。

1.4.6、应用程序类数据共享

在 Java 5 中就已经引入了类数据共享机制 (Class Data Sharing,简称 CDS),允许将一组类预处理为共享归档文件,以便在运行时能够进行内存映射以减少 Java 程序的启动时间,当多个 Java 虚拟机(JVM)共享相同的归档文件时,还可以减少动态内存的占用量,同时减少多个虚拟机在同一个物理或虚拟的机器上运行时的资源占用。简单来说,Java 安装程序会把 rt.jar 中的核心类提前转化成内部表示,转储到一个共享存档(shared archive)中。多个 Java 进程(或者说 JVM 实例)可以共享这部分数据。为改善启动和占用空间,Java 10 在现有的 CDS 功能基础上再次拓展,以允许应用类放置在共享存档中。

CDS 特性在原来的 bootstrap 类基础之上,扩展加入了应用类的 CDS (Application Class-Data Sharing) 支持。

原理为:在启动时记录加载类的过程,写入到文本文件中,再次启动时直接读取此启动文本并加载。设想如果应用环境没有大的变化,启动速度就会得到提升。

可以想像为类似于操作系统的休眠过程,合上电脑时把当前应用环境写入磁盘,再次使用时就可以快速恢复环境。

对大型企业应用程序的内存使用情况的分析表明,此类应用程序通常会将数以万计的类加载到应用程序类加载器中,如果能够将 AppCDS 应用于这些应用,将为每个 JVM 进程节省数十乃至数百兆字节的内存。另外对于云平台上的微服务分析表明,许多服务器在启动时会加载数千个应用程序类,AppCDS 可以让这些服务快速启动并改善整个系统响应时间。

1.4.7、线程-局部管控

在已有的 Java 版本中,JVM 线程只能全部启用或者停止,没法做到对单独某个线程的操作。为了能够对单独的某个线程进行操作,Java 10 中线程管控引入 JVM 安全点的概念,将允许在不运行全局 JVM 安全点的情况下实现线程回调,由线程本身或者 JVM 线程来执行,同时保持线程处于阻塞状态,这种方式使得停止单个线程变成可能,而不是只能启用或停止所有线程。通过这种方式显著地提高了现有 JVM 功能的性能开销,并且改变了到达 JVM 全局安全点的现有时间语义。

增加的参数为:-XX:ThreadLocalHandshakes (默认为开启),将允许用户在支持的平台上选择安全点。

1.4.8、移除 Native-Header 自动生成工具

自 Java 9 以来便开始了一些对 JDK 的调整,用户每次调用 javah 工具时会被警告该工具在未来的版本中将会执行的删除操作。当编译 JNI 代码时,已不再需要单独的 Native-Header 工具来生成头文件,因为这可以通过 Java 8(JDK-7150368)中添加的 javac 来完成。在未来的某一时刻,JNI 将会被 Panama 项目的结果取代,但是何时发生还没有具体时间表。

1.4.9、额外的 Unicode 语言标签扩展

自 Java 7 开始支持 BCP 47 语言标记以来, JDK 中便增加了与日历和数字相关的 Unicode 区域设置扩展,在 Java 9 中,新增支持 ca 和 nu 两种语言标签扩展。而在 Java 10 中将继续增加 Unicode 语言标签扩展,具体为:增强 java.util.Locale 类及其相关的 API,以更方便的获得所需要的语言地域环境信息。同时在这次升级中还带来了如下扩展支持:

表 1.Unicode 扩展表

编码注释
cu货币类型
fw一周的第一天
rg区域覆盖
tz时区

如 Java 10 加入的一个方法:

清单 4. Unicode 语言标签扩展示例

java.time.format.DateTimeFormatter::localizedBy

通过这个方法,可以采用某种数字样式,区域定义或者时区来获得时间信息所需的语言地域本地环境信息。

1.4.10、备用存储装置上的堆分配

硬件技术在持续进化,现在可以使用与传统 DRAM 具有相同接口和类似性能特点的非易失性 RAM。Java 10 中将使得 JVM 能够使用适用于不同类型的存储机制的堆,在可选内存设备上进行堆内存分配。

一些操作系统中已经通过文件系统提供了使用非 DRAM 内存的方法。例如:NTFS DAX 模式和 ext4 DAX。这些文件系统中的内存映射文件可绕过页面缓存并提供虚拟内存与设备物理内存的相互映射。与 DRAM 相比,NV-DIMM 可能具有更高的访问延迟,低优先级进程可以为堆使用 NV-DIMM 内存,允许高优先级进程使用更多 DRAM。

要在这样的备用设备上进行堆分配,可以使用堆分配参数 -XX:AllocateHeapAt = <path> ,这个参数将指向文件系统的文件并使用内存映射来达到在备用存储设备上进行堆分配的预期结果。

1.4.11、基于 Java 的 实验性 JIT 编译器

Java 10 中开启了基于 Java 的 JIT 编译器 Graal,并将其用作 Linux/x64 平台上的实验性 JIT 编译器开始进行测试和调试工作,另外 Graal 将使用 Java 9 中引入的 JVM 编译器接口(JVMCI)。

Graal 是一个以 Java 为主要编程语言、面向 Java bytecode 的编译器。与用 C++实现的 C1 及 C2 相比,它的模块化更加明显,也更加容易维护。Graal 既可以作为动态编译器,在运行时编译热点方法;亦可以作为静态编译器,实现 AOT 编译。在 Java 10 中,Graal 作为试验性 JIT 编译器一同发布(JEP 317)。将 Graal 编译器研究项目引入到 Java 中,或许能够为 JVM 性能与当前 C++ 所写版本匹敌(或有幸超越)提供基础。

Java 10 中默认情况下 HotSpot 仍使用的是 C2 编译器,要启用 Graal 作为 JIT 编译器,请在 Java 命令行上使用以下参数:

清单 5. 启用 Graal 为 JIT 编译器示例

-XX:+ UnlockExperimentalVMOptions -XX:+ UseJVMCICompiler

1.4.12、根证书认证

自 Java 9 起在 keytool 中加入参数 -cacerts ,可以查看当前 JDK 管理的根证书。而 Java 9 中 cacerts 目录为空,这样就会给开发者带来很多不便。从 Java 10 开始,将会在 JDK 中提供一套默认的 CA 根证书。

作为 JDK 一部分的 cacerts 密钥库旨在包含一组能够用于在各种安全协议的证书链中建立信任的根证书。但是,JDK 源代码中的 cacerts 密钥库至目前为止一直是空的。因此,在 JDK 构建中,默认情况下,关键安全组件(如 TLS)是不起作用的。要解决此问题,用户必须使用一组根证书配置和 cacerts 密钥库下的 CA 根证书。

1.4.13、基于时间的版本发布模式

虽然 JEP 223 中引入的版本字符串方案较以往有了显著的改进。但是,该方案并不适合以后严格按照六个月的节奏来发布 Java 新版本的这种情况。

按照 JEP 223 的语义中,每个基于 JDK 构建或使用组件的开发者(包括 JDK 的发布者)都必须提前敲定版本号,然后切换过去。开发人员则必须在代码中修改检查版本号的相关代码,这对所有参与者来说都很尴尬和混乱。

Java 10 中将重新编写之前 JDK 版本中引入的版本号方案,将使用基于时间模型定义的版本号格式来定义新版本。保留与 JEP 223 版本字符串方案的兼容性,同时也允许除当前模型以外的基于时间的发布模型。使开发人员或终端用户能够轻松找出版本的发布时间,以便开发人员能够判断是否将其升级到具有最新安全修补程序或可能的附加功能的新版本。

Oracle Java 平台组的首席架构师 Mark Reinhold 在博客上介绍了有关 Java 未来版本的一些想法(你能接受 Java 9 的下一个版本是 Java 18.3 吗?)。他提到,Java 计划按照时间来发布,每半年一个版本,而不是像之前那样按照重要特性来确定大版本,如果某个大的特性因故延期,这个版本可能一拖再拖。

当时,Mark 也提出来一种基于时间命名版本号的机制,比如下一个将于 2018 年 3 月发布的版本,就是 18.3,再下一个版本是 18.9,以后版本依此类推。

不过经过讨论,考虑和之前版本号的兼容等问题,最终选择的命名机制是:

$FEATURE.$INTERIM.$UPDATE.$PATCH
  • $FEATURE:每次版本发布加 1,不考虑具体的版本内容。2018 年 3 月的版本是 JDK 10,9 月的版本是 JDK 11,依此类推。
  • $INTERIM:中间版本号,在大版本中间发布的,包含问题修复和增强的版本,不会引入非兼容性修改。

1.4.14、结束语

尽管距离 Java 9 发布仅有六个月的时间,Java 10 的发布也带来了不少新特性和功能增强,以上只是针对其中对开发人员影响重大的主要的一些特性做了介绍,同时也希望下一个 Java 版本能够带来更多、更大的变化。以上只是个人在实际项目中的一点思考,如有不足之处,还望各位读者能够海涵,如可以,希望读者们能够反馈意见,交流心得,一同进步。

1.5、Java 11 新特性概述

Java 11 已于 2018 年 9 月 25 日正式发布,之前在 Java 10 新特性介绍 中介绍过,为了加快的版本迭代、跟进社区反馈,Java 的版本发布周期调整为每六个月一次——即每半年发布一个大版本,每个季度发布一个中间特性版本,并且做出不会跳票的承诺。通过这样的方式,Java 开发团队能够将一些重要特性尽早的合并到 Java Release 版本中,以便快速得到开发者的反馈,避免出现类似 Java 9 发布时的两次延期的情况。

按照官方介绍,新的版本发布周期将会严格按照时间节点,于每年的 3 月和 9 月发布,Java 11 发布的时间节点也正好处于 Java 8 免费更新到期的前夕。与 Java 9 和 Java 10 这两个被称为”功能性的版本”不同,Java 11 仅将提供长期支持服务(LTS, Long-Term-Support),还将作为 Java 平台的默认支持版本,并且会提供技术支持直至 2023 年 9 月,对应的补丁和安全警告等支持将持续至 2026 年。

本文主要针对 Java 11 中的新特性展开介绍,让您快速了解 Java 11 带来的变化。

知识体系系统性梳理

1.5.1、基于嵌套的访问控制

与 Java 语言中现有的嵌套类型概念一致, 嵌套访问控制是一种控制上下文访问的策略,允许逻辑上属于同一代码实体,但被编译之后分为多个分散的 class 文件的类,无需编译器额外的创建可扩展的桥接访问方法,即可访问彼此的私有成员,并且这种改进是在 Java 字节码级别的。

在 Java 11 之前的版本中,编译之后的 class 文件中通过 InnerClasses 和 Enclosing Method 两种属性来帮助编译器确认源码的嵌套关系,每一个嵌套的类会编译到自己所在的 class 文件中,不同类的文件通过上面介绍的两种属性的来相互连接。这两种属性对于编译器确定相互之间的嵌套关系已经足够了,但是并不适用于访问控制。这里大家可以写一段包含内部类的代码,并将其编译成 class 文件,然后通过 javap 命令行来分析,碍于篇幅,这里就不展开讨论了。

Java 11 中引入了两个新的属性:一个叫做 NestMembers 的属性,用于标识其它已知的静态 nest 成员;另外一个是每个 nest 成员都包含的 NestHost 属性,用于标识出它的 nest 宿主类。

1.5.2、标准 HTTP Client 升级

Java 11 对 Java 9 中引入并在 Java 10 中进行了更新的 Http Client API 进行了标准化,在前两个版本中进行孵化的同时,Http Client 几乎被完全重写,并且现在完全支持异步非阻塞。

新版 Java 中,Http Client 的包名由 jdk.incubator.http 改为 java.net.http,该 API 通过 CompleteableFutures 提供非阻塞请求和响应语义,可以联合使用以触发相应的动作,并且 RX Flo w 的概念也在 Java 11 中得到了实现。现在,在用户层请求发布者和响应发布者与底层套接字之间追踪数据流更容易了。这降低了复杂性,并最大程度上提高了 HTTP/1 和 HTTP/2 之间的重用的可能性。

Java 11 中的新 Http Client API,提供了对 HTTP/2 等业界前沿标准的支持,同时也向下兼容 HTTP/1.1,精简而又友好的 API 接口,与主流开源 API(如:Apache HttpClient、Jetty、OkHttp 等)类似甚至拥有更高的性能。与此同时它是 Java 在 Reactive-Stream 方面的第一个生产实践,其中广泛使用了 Java Flow API,终于让 Java 标准 HTTP 类库在扩展能力等方面,满足了现代互联网的需求,是一个难得的现代 Http/2 Client API 标准的实现,Java 工程师终于可以摆脱老旧的 HttpURLConnection 了。下面模拟 Http GET 请求并打印返回内容:

清单 1. GET 请求示例

  1. HttpClient client = HttpClient.newHttpClient();
  2. HttpRequest request = HttpRequest.newBuilder()
  3. .uri(URI.create("http://openjdk.java.net/"))
  4. .build();
  5. client.sendAsync(request, BodyHandlers.ofString())
  6. .thenApply(HttpResponse::body)
  7. .thenAccept(System.out::println)
  8. .join();

1.5.3、Epsilon:低开销垃圾回收器

Epsilon 垃圾回收器的目标是开发一个控制内存分配,但是不执行任何实际的垃圾回收工作。它提供一个完全消极的 GC 实现,分配有限的内存资源,最大限度的降低内存占用和内存吞吐延迟时间。

Java 版本中已经包含了一系列的高度可配置化的 GC 实现。各种不同的垃圾回收器可以面对各种情况。但是有些时候使用一种独特的实现,而不是将其堆积在其他 GC 实现上将会是事情变得更加简单。

下面是 no-op GC 的几个使用场景:

  • 性能测试:什么都不执行的 GC 非常适合用于 GC 的差异性分析。no-op (无操作)GC 可以用于过滤掉 GC 诱发的性能损耗,比如 GC 线程的调度,GC 屏障的消耗,GC 周期的不合适触发,内存位置变化等。此外有些延迟者不是由于 GC 引起的,比如 scheduling hiccups, compiler transition hiccups,所以去除 GC 引发的延迟有助于统计这些延迟。
  • 内存压力测试:在测试 Java 代码时,确定分配内存的阈值有助于设置内存压力常量值。这时 no-op 就很有用,它可以简单地接受一个分配的内存分配上限,当内存超限时就失败。例如:测试需要分配小于 1G 的内存,就使用-Xmx1g 参数来配置 no-op GC,然后当内存耗尽的时候就直接 crash。
  • VM 接口测试:以 VM 开发视角,有一个简单的 GC 实现,有助于理解 VM-GC 的最小接口实现。它也用于证明 VM-GC 接口的健全性。
  • 极度短暂 job 任务:一个短声明周期的 job 任务可能会依赖快速退出来释放资源,这个时候接收 GC 周期来清理 heap 其实是在浪费时间,因为 heap 会在退出时清理。并且 GC 周期可能会占用一会时间,因为它依赖 heap 上的数据量。 延迟改进:对那些极端延迟敏感的应用,开发者十分清楚内存占用,或者是几乎没有垃圾回收的应用,此时耗时较长的 GC 周期将会是一件坏事。
  • 吞吐改进:即便对那些无需内存分配的工作,选择一个 GC 意味着选择了一系列的 GC 屏障,所有的 OpenJDK GC 都是分代的,所以他们至少会有一个写屏障。避免这些屏障可以带来一点点的吞吐量提升。

Epsilon 垃圾回收器和其他 OpenJDK 的垃圾回收器一样,可以通过参数 -XX:+UseEpsilonGC 开启。

Epsilon 线性分配单个连续内存块。可复用现存 VM 代码中的 TLAB 部分的分配功能。非 TLAB 分配也是同一段代码,因为在此方案中,分配 TLAB 和分配大对象只有一点点的不同。Epsilon 用到的 barrier 是空的(或者说是无操作的)。因为该 GC

执行任何的 GC 周期,不用关系对象图,对象标记,对象复制等。引进一种新的 barrier-set 实现可能是该 GC 对 JVM 最大的变化。

1.5.4、简化启动单个源代码文件的方法

Java 11 版本中最令人兴奋的功能之一是增强 Java 启动器,使之能够运行单一文件的 Java 源代码。此功能允许使用 Java 解释器直接执行 Java 源代码。源代码在内存中编译,然后由解释器执行。唯一的约束在于所有相关的类必须定义在同一个 Java 文件中。

此功能对于开始学习 Java 并希望尝试简单程序的人特别有用,并且能与 jshell 一起使用,将成为任何初学者学习语言的一个很好的工具集。不仅初学者会受益,专业人员还可以利用这些工具来探索新的语言更改或尝试未知的 API。

如今单文件程序在编写小实用程序时很常见,特别是脚本语言领域。从中开发者可以省去用 Java 编译程序等不必要工作,以及减少新手的入门障碍。在基于 Java 10 的程序实现中可以通过三种方式启动:

  • 作为 * .class 文件
  • 作为 * .jar 文件中的主类
  • 作为模块中的主类

而在最新的 Java 11 中新增了一个启动方式,即可以在源代码中声明类,例如:如果名为 HelloWorld.java 的文件包含一个名为 hello.World 的类,那么该命令:

$ java HelloWorld.java

也等同于:

  1. $ javac HelloWorld.java
  2. $ java -cp . hello.World

1.5.5、用于 Lambda 参数的局部变量语法

在 Lambda 表达式中使用局部变量类型推断是 Java 11 引入的唯一与语言相关的特性,这一节,我们将探索这一新特性。

从 Java 10 开始,便引入了局部变量类型推断这一关键特性。类型推断允许使用关键字 var 作为局部变量的类型而不是实际类型,编译器根据分配给变量的值推断出类型。这一改进简化了代码编写、节省了开发者的工作时间,因为不再需要显式声明局部变量的类型,而是可以使用关键字 var,且不会使源代码过于复杂。

可以使用关键字 var 声明局部变量,如下所示:

  1. var s = "Hello Java 11";
  2. System.out.println(s);

但是在 Java 10 中,还有下面几个限制:

  • 只能用于局部变量上
  • 声明时必须初始化
  • 不能用作方法参数
  • 不能在 Lambda 表达式中使用

Java 11 与 Java 10 的不同之处在于允许开发者在 Lambda 表达式中使用 var 进行参数声明。乍一看,这一举措似乎有点多余,因为在写代码过程中可以省略 Lambda 参数的类型,并通过类型推断确定它们。但是,添加上类型定义同时使用 @Nonnull 和 @Nullable 等类型注释还是很有用的,既能保持与局部变量的一致写法,也不丢失代码简洁。

Lambda 表达式使用隐式类型定义,它形参的所有类型全部靠推断出来的。隐式类型 Lambda 表达式如下:

(x, y) -> x.process(y)

Java 10 为局部变量提供隐式定义写法如下:

  1. var x = new Foo();
  2. for (var x : xs) { ... }
  3. try (var x = ...) { ... } catch ...

为了 Lambda 类型表达式中正式参数定义的语法与局部变量定义语法的不一致,且为了保持与其他局部变量用法上的一致性,希望能够使用关键字 var 隐式定义 Lambda 表达式的形参:

(var x, var y) -> x.process(y)

于是在 Java 11 中将局部变量和 Lambda 表达式的用法进行了统一,并且可以将注释应用于局部变量和 Lambda 表达式:

  1. @Nonnull var x = new Foo();
  2. (@Nonnull var x, @Nullable var y) -> x.process(y)

1.5.6、低开销的 Heap Profiling

Java 11 中提供一种低开销的 Java 堆分配采样方法,能够得到堆分配的 Java 对象信息,并且能够通过 JVMTI 访问堆信息。

引入这个低开销内存分析工具是为了达到如下目的:

  • 足够低的开销,可以默认且一直开启
  • 能通过定义好的程序接口访问
  • 能够对所有堆分配区域进行采样
  • 能给出正在和未被使用的 Java 对象信息

对用户来说,了解它们堆里的内存分布是非常重要的,特别是遇到生产环境中出现的高 CPU、高内存占用率的情况。目前有一些已经开源的工具,允许用户分析应用程序中的堆使用情况,比如:Java Flight Recorder、jmap、YourKit 以及 VisualVM tools.。但是这些工具都有一个明显的不足之处:无法得到对象的分配位置,headp dump 以及 heap histogram 中都没有包含对象分配的具体信息,但是这些信息对于调试内存问题至关重要,因为它能够告诉开发人员他们的代码中发生的高内存分配的确切位置,并根据实际源码来分析具体问题,这也是 Java 11 中引入这种低开销堆分配采样方法的原因。

1.5.7、支持 TLS 1.3 协议

Java 11 中包含了传输层安全性(TLS)1.3 规范(RFC 8446)的实现,替换了之前版本中包含的 TLS,包括 TLS 1.2,同时还改进了其他 TLS 功能,例如 OCSP 装订扩展(RFC 6066,RFC 6961),以及会话散列和扩展主密钥扩展(RFC 7627),在安全性和性能方面也做了很多提升。

新版本中包含了 Java 安全套接字扩展(JSSE)提供 SSL,TLS 和 DTLS 协议的框架和 Java 实现。目前,JSSE API 和 JDK 实现支持 SSL 3.0,TLS 1.0,TLS 1.1,TLS 1.2,DTLS 1.0 和 DTLS 1.2。

同时 Java 11 版本中实现的 TLS 1.3,重新定义了以下新标准算法名称:

  • TLS 协议版本名称:TLSv1.3
  • SSLContext 算法名称:TLSv1.3
  • TLS 1.3 的 TLS 密码套件名称:TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384
  • 用于 X509KeyManager 的 keyType:RSASSA-PSS
  • 用于 X509TrustManager 的 authType:RSASSA-PSS

还为 TLS 1.3 添加了一个新的安全属性 jdk.tls.keyLimits。当处理了特定算法的指定数据量时,触发握手后,密钥和 IV 更新以导出新密钥。还添加了一个新的系统属性 jdk.tls.server.protocols,用于在 SunJSSE 提供程序的服务器端配置默认启用的协议套件。

之前版本中使用的 KRB5 密码套件实现已从 Java 11 中删除,因为该算法已不再安全。同时注意,TLS 1.3 与以前的版本不直接兼容。

升级到 TLS 1.3 之前,需要考虑如下几个兼容性问题:

  • TLS 1.3 使用半关闭策略,而 TLS 1.2 以及之前版本使用双工关闭策略,对于依赖于双工关闭策略的应用程序,升级到 TLS 1.3 时可能存在兼容性问题。
  • TLS 1.3 使用预定义的签名算法进行证书身份验证,但实际场景中应用程序可能会使用不被支持的签名算法。
  • TLS 1.3 再支持 DSA 签名算法,如果在服务器端配置为仅使用 DSA 证书,则无法升级到 TLS 1.3。
  • TLS 1.3 支持的加密套件与 TLS 1.2 和早期版本不同,若应用程序硬编码了加密算法单元,则在升级的过程中需要修改相应代码才能升级使用 TLS 1.3。
  • TLS 1.3 版本的 session 用行为及秘钥更新行为与 1.2 及之前的版本不同,若应用依赖于 TLS 协议的握手过程细节,则需要注意。

1.5.8、ZGC:可伸缩低延迟垃圾收集器

ZGC 即 Z Garbage Collector(垃圾收集器或垃圾回收器),这应该是 Java 11 中最为瞩目的特性,没有之一。ZGC 是一个可伸缩的、低延迟的垃圾收集器,主要为了满足如下目标进行设计:

  • GC 停顿时间不超过 10ms
  • 即能处理几百 MB 的小堆,也能处理几个 TB 的大堆
  • 应用吞吐能力不会下降超过 15%(与 G1 回收算法相比)
  • 方便在此基础上引入新的 GC 特性和利用 colord
  • 针以及 Load barriers 优化奠定基础
  • 当前只支持 Linux/x64 位平台 停顿时间在 10ms 以下,10ms 其实是一个很保守的数据,即便是 10ms 这个数据,也是 GC 调优几乎达不到的极值。根据 SPECjbb 2015 的基准测试,128G 的大堆下最大停顿时间才 1.68ms,远低于 10ms,和 G1 算法相比,改进非常明显。

本图片引用自: The Z Garbage Collector – An Introduction

不过目前 ZGC 还处于实验阶段,目前只在 Linux/x64 上可用,如果有足够的需求,将来可能会增加对其他平台的支持。同时作为实验性功能的 ZGC 将不会出现在 JDK 构建中,除非在编译时使用 configure 参数: --with-jvm-features=zgc 显式启用。

在实验阶段,编译完成之后,已经迫不及待的想试试 ZGC,需要配置以下 JVM 参数,才能使用 ZGC,具体启动 ZGC 参数如下:

-XX:+ UnlockExperimentalVMOptions -XX:+ UseZGC -Xmx10g

其中参数: -Xmx 是 ZGC 收集器中最重要的调优选项,大大解决了程序员在 JVM 参数调优上的困扰。ZGC 是一个并发收集器,必须要设置一个最大堆的大小,应用需要多大的堆,主要有下面几个考量:

  • 对象的分配速率,要保证在 GC 的时候,堆中有足够的内存分配新对象。
  • 一般来说,给 ZGC 的内存越多越好,但是也不能浪费内存,所以要找到一个平衡。

1.5.9、飞行记录器

飞行记录器之前是商业版 JDK 的一项分析工具,但在 Java 11 中,其代码被包含到公开代码库中,这样所有人都能使用该功能了。

Java 语言中的飞行记录器类似飞机上的黑盒子,是一种低开销的事件信息收集框架,主要用于对应用程序和 JVM 进行故障检查、分析。飞行记录器记录的主要数据源于应用程序、JVM 和 OS,这些事件信息保存在单独的事件记录文件中,故障发生后,能够从事件记录文件中提取出有用信息对故障进行分析。

启用飞行记录器参数如下:

-XX:StartFlightRecording

也可以使用 bin/jcmd 工具启动和配置飞行记录器:

清单 2. 飞行记录器启动、配置参数示例

  1. $ jcmd <pid> JFR.start
  2. $ jcmd <pid> JFR.dump filename=recording.jfr
  3. $ jcmd <pid> JFR.stop

JFR 使用测试

清单 3. JFR 使用示例

  1. public class FlightRecorderTest extends Event {
  2. @Label("Hello World")
  3. @Description("Helps the programmer getting started")
  4. static class HelloWorld extends Event {
  5. @Label("Message")
  6. String message;
  7. }
  8. public static void main(String[] args) {
  9. HelloWorld event = new HelloWorld();
  10. event.message = "hello, world!";
  11. event.commit();
  12. }
  13. }

在运行时加上如下参数:

java -XX:StartFlightRecording=duration=1s, filename=recording.jfr

下面读取上一步中生成的 JFR 文件:recording.jfr

清单 4. 飞行记录器分析示例

  1. public void readRecordFile() throws IOException {
  2. final Path path = Paths.get("D:\\ java \\recording.jfr");
  3. final List<RecordedEvent> recordedEvents = RecordingFile.readAllEvents(path);
  4. for (RecordedEvent event : recordedEvents) {
  5. System.out.println(event.getStartTime() + "," + event.getValue("message"));
  6. }
  7. }

 1.5.10、动态类文件常量

为了使 JVM 对动态语言更具吸引力,Java 的第七个版本已将 invokedynamic 引入其指令集。

不过 Java 开发人员通常不会注意到此功能,因为它隐藏在 Java 字节代码中。通过使用 invokedynamic,可以延迟方法调用的绑定,直到第一次调用。例如,Java 语言使用该技术来实现 Lambda 表达式,这些表达式仅在首次使用时才显示出来。这样做,invokedynamic 已经演变成一种必不可少的语言功能。

Java 11 引入了类似的机制,扩展了 Java 文件格式,以支持新的常量池:CONSTANT_Dynamic,它在初始化的时候,像 invokedynamic 指令生成代理方法一样,委托给 bootstrap 方法进行初始化创建,对上层软件没有很大的影响,降低开发新形式的可实现类文件约束带来的成本和干扰。

1.5.11、结束语

Java 在更新发布周期为每半年发布一次之后,在合并关键特性、快速得到开发者反馈等方面,做得越来越好。Java 11 版本的发布也带来了不少新特性和功能增强、性能提升、基础能力的全面进步和突破,本文针对其中对使用人员影响重大的以及主要的特性做了介绍。

1.6、Java 12 新特性概述

JDK12 在 2019 年 3 月 19 号正式发布,不同于JDK11,JDK12并不是一个LTS版本。作为一个中间版本,JDK12版本特性增加较少。 2017年宣布的加速发布节奏要求每六个月发布一次功能,每季度更新一次,每三年发布一次长期支持(LTS)更新版本(或每六个版本一次)

知识体系系统性梳理

1.6.1、新功能和库的更新

1.6.1.1、JEP334: JVM常量API

每个Java类文件都有一个常量池,该池存储该类中字节码指令的操作。广义上讲,常量池中的条目要么描述运行时artifacts(例如类和方法),要么描述简单值(例如字符串和整数)。

所有这些条目都称为可加载常量,因为它们可以用作ldc指令的参数(“加载常量”)。它们也可能出现在invokedynamic指令的引导方法的静态参数列表中。执行ldc或invokedynamic指令会导致将可加载常量解析为标准Java类型(如Class,String或int)的“实时”值。

处理类文件的程序需要对字节码指令进行建模,然后对可加载常量进行建模。但是,使用标准Java类型对可加载常量进行建模是不够的。

描述一个字符串(一个CONSTANT_String_info条目)的可加载常量可能是可以接受的,因为生成一个“live” String对象是很简单的,但是对于描述一个类(一个CONSTANT_Class_info条目)的一个可加载常量来说,这是有问题的,因为生成一个“live”类对象依赖于类加载的正确性和一致性。

在实际应用中,类加载具有许多环境依赖性和失败的情况,例如:所需的类不存在或请求者可能无法访问;类加载的结果随上下文而变化;加载类具有副作用;有时根本不可能加载类。

因此,处理可加载常量的程序如果能够操纵类和方法,并且以纯名义上的符号形式操纵诸如方法句柄和动态计算的常量之类的artifacts,则它们将变得更加简单。

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

1.6.1.2、JEP341: 默认CDS归档

通过在64位平台上的默认类列表的帮助下生成CDS归档来改进JDK构建过程,从而有效地消除了运行java -Xshare:dump。 此功能的目标包括:

  • 改进开箱即用的启动时间
  • 摆脱使用-Xshare:dump
1.6.1.3、JEP230: Microbenchmark测试套件

此功能为JDK源代码添加了一套Microbenchmark测试(大约100个),简化了现有Microbenchmark测试的运行和新基准测试的创建过程。 它基于Java Microbenchmark Harness(JMH)并支持JMH更新。

此功能使开发人员可以轻松运行当前的Microbenchmark测试并为JDK源代码添加新的Microbenchmark测试。 可以基于Java Microbenchmark Harness(JMH)轻松测试JDK性能。 它将支持JMH更新,并在套件中包含一组(约100个)基准测试。

1.6.2、新的平台支持

1.6.2.1、JEP340: 移除多余ARM64实现

Java 12将只有一个ARM 64位实现(aarch64)。 目标是删除所有与arm64实现相关的代码,同时保留32位ARM端口和64位aarch64实现。

这将把重点转移到单个64位ARM实现,并消除维护两个实现所需的重复工作。 当前的JDK 11实现中有两个64位ARM实现。

1.6.3、JVM 优化

1.6.3.1、JPE 344: G1的可中断 mixed GC

此功能通过将Mixed GC集拆分为强制部分和可选部分,使G1垃圾收集器更有效地中止垃圾收集过程。通过允许垃圾收集过程优先处理强制集,g1可以更多满足满足暂停时间目标。

G1是一个垃圾收集器,设计用于具有大量内存的多处理器机器。由于它提高了性能效率,g1垃圾收集器最终将取代cms垃圾收集器。

G1垃圾收集器的主要目标之一是满足用户设置的暂停时间。G1采用一个分析引擎来选择在收集期间要处理的工作量。此选择过程的结果是一组称为GC集的区域。一旦GC集建立并且GC已经开始,那么G1就无法停止。

如果G1发现GC集选择选择了错误的区域,它会将GC区域的拆分为两部分(强制部分和可选部分)来切换到处理Mix GC的增量模式。如果未达到暂停时间目标,则停止对可选部分的垃圾收集。

1.6.3.2、JEP 346: G1归还不使用的内存

此功能的主要目标是改进G1垃圾收集器,以便在不活动时将Java堆内存归还给操作系统。 为实现此目标,G1将在低应用程序活动期间定期生成或持续循环检查完整的Java堆使用情况。

这将立即归还未使用的部分Java堆内存给操作系统。 用户可以选择执行FULL GC以最大化返回的内存量。

1.6.4、新功能的预览和实验

1.6.4.1、JEP 189: Shenandoah:低暂停时间垃圾收集器(实验)

JDK 12 引入的回收算法(实验阶段),该算法通过与正在运行的 Java 线程同时进行疏散工作来减少 GC 暂停时间。Shenandoah 的暂停时间与堆大小无关,无论堆栈是 200 MB 还是 200 GB,都具有相同的一致暂停时间。

Shenandoah适用于高吞吐和大内存场景,不适合高实时性场景。Shenandoah算法设计目标主要是响应性和一致可控的短暂停顿,对于垃圾回收生命周期中安全点停顿(TTSP)和内存增长监控的时间开销并无帮助。

Shenandoah算法为每个Java对象添加了一个间接指针,使得GC线程能够在Java线程运行时压缩堆。标记和压缩是同时执行的,因此我们只需要暂停Java线程在一致可控的时间内扫描线程堆栈以查找和更新对象图的根。

怎么形容Shenandoah和ZGC的关系呢?异同点大概如下:

  • 相同点:性能几乎可认为是相同的
  • 不同点:ZGC是Oracle JDK的。而Shenandoah只存在于OpenJDK中,因此使用时需注意你的JDK版本
  • 打开方式:使用-XX:+UseShenandoahGC命令行参数打开。
1.6.4.2、JEP 325: Switch 表达式 (预览版本)

在 Java 12 中引入了 Switch 表达式作为预览特性

在 Java 12 之前,传统 Switch 语句写法为:

  1. private static String getText(int number) {
  2. String result = "";
  3. switch (number) {
  4. case 1, 2:
  5. result = "one or two";
  6. break;
  7. case 3:
  8. result = "three";
  9. break;
  10. case 4, 5, 6:
  11. result = "four or five or six";
  12. break;
  13. default:
  14. result = "unknown";
  15. break;
  16. };
  17. return result;
  18. }

在 Java 12 之后,关于 Switch 表达式的写法改进为如下:

  1. private static String getText(int number) {
  2. String result = switch (number) {
  3. case 1, 2 -> "one or two";
  4. case 3 -> "three";
  5. case 4, 5, 6 -> "four or five or six";
  6. default -> "unknown";
  7. };
  8. return result;
  9. }

1.7、Java 13 新特性概述

Java 13 已如期于 9 月 17 日正式发布,此次更新是继半年前 Java 12 这大版本发布之后的一次常规版本更新,在这一版中,主要带来了 ZGC 增强、更新 Socket 实现、Switch 表达式更新等方面的改动、增强。本文主要针对 Java 13 中主要的新特性展开介绍,带你快速了解 Java 13 带来的不同体验。

知识体系系统性梳理

1.7.1、新功能和库的更新

1.7.1.1、JEP350: 动态应用程序类-数据共享

在 Java 10 中,为了改善应用启动时间和内存空间占用,通过使用 APP CDS,加大了 CDS 的使用范围,允许自定义的类加载器也可以加载自定义类给多个 JVM 共享使用,具体介绍可以参考 Java 10 新特性介绍一文详细介绍,在此就不再继续展开。

Java 13 中对 Java 10 中引入的 应用程序类数据共享进行了进一步的简化、改进和扩展,即:允许在 Java 应用程序执行结束时动态进行类归档,具体能够被归档的类包括:所有已被加载,但不属于默认基层 CDS 的应用程序类和引用类库中的类。通过这种改进,可以提高应用程序类-数据使用上的简易性,减少在使用类-数据存档中需要为应用程序创建类加载列表的必要,简化使用类-数据共享的步骤,以便更简单、便捷地使用 CDS 存档。

在 Java 中,如果要执行一个类,首先需要将类编译成对应的字节码文件,以下是 JVM 装载、执行等需要的一系列准备步骤:假设给定一个类名,JVM 将在磁盘上查找到该类对应的字节码文件,并将其进行加载,验证字节码文件,准备,解析,初始化,根据其内部数据结构加载到内存中。当然,这一连串的操作都需要一些时间,这在 JVM 启动并且需要加载至少几百个甚至是数千个类时,加载时间就尤其明显。

Java 10 中的 App CDS 主要是为了将不变的类数据,进行一次创建,然后存储到归档中,以便在应用重启之后可以对其进行内存映射而直接使用,同时也可以在运行的 JVM 实例之间共享使用。但是在 Java 10 中使用 App CDS 需要进行如下操作:

  • 创建需要进行类归档的类列表
  • 创建归档
  • 使用归档方式启动

在使用归档文件启动时,JVM 将归档文件映射到其对应的内存中,其中包含所需的大多数类,而

需要使用多么复杂的类加载机制。甚至可以在并发运行的 JVM 实例之间共享内存区域,通过这种方式可以释放需要在每个 JVM 实例中创建相同信息时浪费的内存,从而节省了内存空间。

在 Java 12 中,默认开启了对 JDK 自带 JAR 包类的存档,如果想关闭对自带类库的存档,可以在启动参数中加上:

-Xshare:off

而在 Java 13 中,可以不用提供归档类列表,而是通过更简洁的方式来创建包含应用程序类的归档。具体可以使用参数 -XX:ArchiveClassesAtExit 来控制应用程序在退出时生成存档,也可以使用 -XX:SharedArchiveFile 来使用动态存档功能,详细使用见如下示例。

清单 1. 创建存档文件示例

$ java -XX:ArchiveClassesAtExit=helloworld.jsa -cp helloworld.jar Hello

清单 2. 使用存档文件示例

$ java -XX:SharedArchiveFile=hello.jsa -cp helloworld.jar Hello

上述就是在 Java 应用程序执行结束时动态进行类归档,并且在 Java 10 的基础上,将多条命令进行了简化,可以更加方便地使用类归档功能。

1.7.1.2、JEP353: Socket API 重构

Java 中的 Socket API 已经存在了二十多年了,尽管这么多年来,一直在维护和更新中,但是在实际使用中遇到一些局限性,并且不容易维护和调试,所以要对其进行大修大改,才能跟得上现代技术的发展,毕竟二十多年来,技术都发生了深刻的变化。Java 13 为 Socket API 带来了新的底层实现方法,并且在 Java 13 中是默认使用新的 Socket 实现,使其易于发现并在排除问题同时增加可维护性。

Java Socket API(java.net.ServerSocket 和 java.net.Socket)包含允许监听控制服务器和发送数据的套接字对象。可以使用 ServerSocket 来监听连接请求的端口,一旦连接成功就返回一个 Socket 对象,可以使用该对象读取发送的数据和进行数据写回操作,而这些类的繁重工作都是依赖于 SocketImpl 的内部实现,服务器的发送和接收两端都基于 SOCKS 进行实现的。

在 Java 13 之前,通过使用 PlainSocketImpl 作为 SocketImpl 的具体实现。

Java 13 中的新底层实现,引入 NioSocketImpl 的实现用以替换 SocketImpl 的 PlainSocketImpl 实现,此实现与 NIO(新 I/O)实现共享相同的内部基础结构,并且与现有的缓冲区高速缓存机制集成在一起,因此不需要使用线程堆栈。除了这些更改之外,还有其他一些更便利的更改,如使用 java.lang.ref.Cleaner 机制来关闭套接字(如果 SocketImpl 实现在尚未关闭的套接字上被进行了垃圾收集),以及在轮询时套接字处于非阻塞模式时处理超时操作等方面。

为了最小化在重新实现已使用二十多年的方法时出现问题的风险,在引入新实现方法的同时,之前版本的实现还未被移除,可以通过使用下列系统属性以重新使用原实现方法:

-Djdk.net.usePlainSocketImpl = true

另外需要注意的是,SocketImpl 是一种传统的 SPI 机制,同时也是一个抽象类,并未指定具体的实现,所以,新的实现方式尝试模拟未指定的行为,以达到与原有实现兼容的目的。但是,在使用新实现时,有些基本情况可能会失败,使用上述系统属性可以纠正遇到的问题,下面两个除外。

  • 老版本中,PlainSocketImpl 中的 getInputStream() 和 getOutputStream() 方法返回的 InputStream 和 OutputStream 分别来自于其对应的扩展类型 FileInputStream 和 FileOutputStream,而这个在新版实现中则没有。
  • 使用自定义或其它平台的 SocketImpl 的服务器套接字无法接受使用其他(自定义或其它平台)类型 SocketImpl 返回 Sockets 的连接。

通过这些更改,Java Socket API 将更易于维护,更好地维护将使套接字代码的可靠性得到改善。同时 NIO 实现也可以在基础层面完成,从而保持 Socket 和 ServerSocket 类层面上的不变。

1.7.2、JVM 优化

1.7.2.1、JEP351: 增强 ZGC 释放未使用内存

ZGC 是 Java 11 中引入的最为瞩目的垃圾回收特性,是一种可伸缩、低延迟的垃圾收集器,不过在 Java 11 中是实验性的引入,主要用来改善 GC 停顿时间,并支持几百 MB 至几个 TB 级别大小的堆,并且应用吞吐能力下降不会超过 15%,目前只支持 Linux/x64 位平台的这样一种新型垃圾收集器。

通过在实际中的使用,发现 ZGC 收集器中并没有像 Hotspot 中的 G1 和 Shenandoah 垃圾收集器一样,能够主动将未使用的内存释放给操作系统的功能。对于大多数应用程序来说,CPU 和内存都属于有限的紧缺资源,特别是现在使用的云上或者虚拟化环境中。如果应用程序中的内存长期处于空闲状态,并且还不能释放给操作系统,这样会导致其他需要内存的应用无法分配到需要的内存,而这边应用分配的内存还处于空闲状态,处于”忙的太忙,闲的太闲”的非公平状态,并且也容易导致基于虚拟化的环境中,因为这些实际并未使用的资源而多付费的情况。由此可见,将未使用内存释放给系统主内存是一项非常有用且亟需的功能。

ZGC 堆由一组称为 ZPages 的堆区域组成。在 GC 周期中清空 ZPages 区域时,它们将被释放并返回到页面缓存 ZPageCache 中,此缓存中的 ZPages 按最近最少使用(LRU)的顺序,并按照大小进行组织。在 Java 13 中,ZGC 将向操作系统返回被标识为长时间未使用的页面,这样它们将可以被其他进程重用。同时释放这些未使用的内存给操作系统不会导致堆大小缩小到参数设置的最小大小以下,如果将最小和最大堆大小设置为相同的值,则不会释放任何内存给操作系统。

Java 13 中对 ZGC 的改进,主要体现在下面几点:

  • 释放未使用内存给操作系统
  • 支持最大堆大小为 16TB
  • 添加参数:-XX:SoftMaxHeapSize 来软限制堆大小

这里提到的是软限制堆大小,是指 GC 应努力是堆大小不要超过指定大小,但是如果实际需要,也还是允许 GC 将堆大小增加到超过 SoftMaxHeapSize 指定值。主要用在下面几种情况:当希望降低堆占用,同时保持应对堆空间临时增加的能力,亦或想保留充足内存空间,以能够应对内存分配,而不会因为内存分配意外增加而陷入分配停滞状态。不应将 SoftMaxHeapSize 设置为大于最大堆大小(-Xmx 的值,如果未在命令行上设置,则此标志应默认为最大堆大小。

Java 13 中,ZGC 内存释放功能,默认情况下是开启的,不过可以使用参数:-XX:-ZUncommit 显式关闭,同时如果将最小堆大小 (-Xms) 配置为等于最大堆大小 (-Xmx),则将隐式禁用此功能。

还可以使用参数:-XX:ZUncommitDelay = <seconds>(默认值为 300 秒)来配置延迟释放,此延迟时间可以指定释放多长时间之前未使用的内存。

1.7.3、新功能预览

1.7.3.1、JEP354: Switch 表达式扩展(预览功能)

在 Java 12 中引入了 Switch 表达式作为预览特性,而在 Java 13 中对 Switch 表达式做了增强改进,在块中引入了 yield 语句来返回值,而不是使用 break。这意味着,Switch 表达式(返回值)应该使用 yield,而 Switch 语句(不返回值)应该使用 break,而在此之前,想要在 Switch 中返回内容,还是比较麻烦的,只不过目前还处于预览状态。

在 Java 13 之后,Switch 表达式中就多了一个关键字用于跳出 Switch 块的关键字 yield,主要用于返回一个值,它和 return 的区别在于:return 会直接跳出当前循环或者方法,而 yield 只会跳出当前 Switch块,同时在使用 yield 时,需要有 default 条件。

在 Java 12 之前,传统 Switch 语句写法为:

清单 3. 传统形式

  1. private static String getText(int number) {
  2. String result = "";
  3. switch (number) {
  4. case 1, 2:
  5. result = "one or two";
  6. break;
  7. case 3:
  8. result = "three";
  9. break;
  10. case 4, 5, 6:
  11. result = "four or five or six";
  12. break;
  13. default:
  14. result = "unknown";
  15. break;
  16. };
  17. return result;
  18. }

在 Java 12 之后,关于 Switch 表达式的写法改进为如下:

清单 4. 标签简化形式

  1. private static String getText(int number) {
  2. String result = switch (number) {
  3. case 1, 2 -> "one or two";
  4. case 3 -> "three";
  5. case 4, 5, 6 -> "four or five or six";
  6. default -> "unknown";
  7. };
  8. return result;
  9. }

而在 Java 13 中,value break 语句不再被编译,而是用 yield 来进行值返回,上述写法被改为如下写法:

清单 5. yield 返回值形式

  1. private static String getText(int number) {
  2. return switch (number) {
  3. case 1, 2:
  4. yield "one or two";
  5. case 3:
  6. yield "three";
  7. case 4, 5, 6:
  8. yield "four or five or six";
  9. default:
  10. yield "unknown";
  11. };
  12. }
1.7.3.2、JEP355: 文本块(预览功能)

一直以来,Java 语言在定义字符串的方式是有限的,字符串需要以双引号开头,以双引号结尾,这导致字符串不能够多行使用,而是需要通过换行转义或者换行连接符等方式来变通支持多行,但这样会增加编辑工作量,同时也会导致所在代码段难以阅读、难以维护。

Java 13 引入了文本块来解决多行文本的问题,文本块以三重双引号开头,并以同样的以三重双引号结尾终止,它们之间的任何内容都被解释为字符串的一部分,包括换行符,避免了对大多数转义序列的需要,并且它仍然是普通的 java.lang.String 对象,文本块可以在 Java 中可以使用字符串文字的任何地方使用,而与编译后的代码没有区别,还增强了 Java 程序中的字符串可读性。并且通过这种方式,可以更直观地表示字符串,可以支持跨越多行,而且不会出现转义的视觉混乱,将可以广泛提高 Java 类程序的可读性和可写性。

在 Java 13 之前,多行字符串写法为:

清单 6. 多行字符串写法

  1. String html ="<html>\n" +
  2. " <body>\n" +
  3. " <p>Hello, World</p>\n" +
  4. " </body>\n" +
  5. "</html>\n";
  6. String json ="{\n" +
  7. " \"name\":\"mkyong\",\n" +
  8. " \"age\":38\n" +
  9. "}\n";

在 Java 13 引入文本块之后,写法为:

清单 7. 多行文本块写法

  1. String html = """
  2. <html>
  3. <body>
  4. <p>Hello, World</p>
  5. </body>
  6. </html>
  7. """;
  8. String json = """
  9. {
  10. "name":"mkyong",
  11. "age":38
  12. }
  13. """;

文本块是作为预览功能引入到 Java 13 中的,这意味着它们不包含在相关的 Java 语言规范中,这样做的好处是方便用户测试功能并提供反馈,后续更新可以根据反馈来改进功能,或者必要时甚至删除该功能,如果该功能立即成为 Java SE 标准的一部分,则进行更改将变得更加困难。重要的是要意识到预览功能不是 beta 形式。

由于预览功能不是规范的一部分,因此有必要为编译和运行时明确启用它们。需要使用下面两个命令行参数来启用预览功能:

清单 8. 启用预览功能

  1. $ javac --enable-preview --release 13 Example.java
  2. $ java --enable-preview Example

1.7.4、结束语

Java 在更新发布周期为每半年发布一次之后,在合并关键特性、快速得到开发者反馈等方面,做得越来越好。从 Java 11 到 Java 13,目前确实是严格保持半年更新的节奏。Java 13 版本的发布带来了些新特性和功能增强、性能提升和改进尝试,不过 Java 13 不是 LTS 版本,本文针对其中对使用人员影响重大的以及主要的特性做了介绍,如有兴趣,您可以自行下载相关代码,继续深入研究。

 1.8、Java 14 新特性概述

Java 14 已如期于 2020 年 3 月 17 日正式发布,此次更新是继半年前 Java 13 这大版本发布之后的又一次常规版本更新,即便在全球疫情如此严峻形势下,依然保持每六个月的版本更新频率,为大家及时带来改进和增强,这一点值得点赞。在这一版中,主要带来了 ZGC 增强、instanceof 增强、Switch 表达式更新为标准版等方面的改动、增强和新功能。本文主要介绍 Java 14 中的主要新特性,带您快速了解 Java 14 带来了哪些不一样的体验和便利。

知识体系系统性梳理

1.8.1、语言特性增强

1.8.1.1、JEP 359: Switch 表达式(正式版)

switch 表达式在之前的 Java 12 和 Java 13 中都是处于预览阶段,而在这次更新的 Java 14 中,终于成为稳定版本,能够正式可用。

switch 表达式带来的不仅仅是编码上的简洁、流畅,也精简了 switch 语句的使用方式,同时也兼容之前的 switch 语句的使用;之前使用 switch 语句时,在每个分支结束之前,往往都需要加上 break 关键字进行分支跳出,以防 switch 语句一直往后执行到整个 switch 语句结束,由此造成一些意想不到的问题。switch 语句一般使用冒号 :来作为语句分支代码的开始,而 switch 表达式则提供了新的分支切换方式,即 -> 符号右则表达式方法体在执行完分支方法之后,自动结束 switch 分支,同时 -> 右则方法块中可以是表达式、代码块或者是手动抛出的异常。以往的 switch 语句写法如下:

清单 9. Switch 语句

  1. int dayOfWeek;
  2. switch (day) {
  3. case MONDAY:
  4. case FRIDAY:
  5. case SUNDAY:
  6. dayOfWeek = 6;
  7. break;
  8. case TUESDAY:
  9. dayOfWeek = 7;
  10. break;
  11. case THURSDAY:
  12. case SATURDAY:
  13. dayOfWeek = 8;
  14. break;
  15. case WEDNESDAY:
  16. dayOfWeek = 9;
  17. break;
  18. default:
  19. dayOfWeek = 0;
  20. break;
  21. }

而现在 Java 14 可以使用 switch 表达式正式版之后,上面语句可以转换为下列写法:

清单 10. Switch 表达式

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

很明显,switch 表达式将之前 switch 语句从编码方式上简化了不少,但是还是需要注意下面几点:

  • 需要保持与之前 switch 语句同样的 case 分支情况。
  • 之前需要用变量来接收返回值,而现在直接使用 yield 关键字来返回 case 分支需要返回的结果。
  • 现在的 switch 表达式中不再需要显式地使用 return、break 或者 continue 来跳出当前分支。
  • 现在不需要像之前一样,在每个分支结束之前加上 break 关键字来结束当前分支,如果不加,则会默认往后执行,直到遇到 break 关键字或者整个 switch 语句结束,在 Java 14 表达式中,表达式默认执行完之后自动跳出,不会继续往后执行。
  • 对于多个相同的 case 方法块,可以将 case 条件并列,而不需要像之前一样,通过每个 case 后面故意不加 break 关键字来使用相同方法块。

使用 switch 表达式来替换之前的 switch 语句,确实精简了不少代码,提高了编码效率,同时也可以规避一些可能由于不太经意而出现的意想不到的情况,可见 Java 在提高使用者编码效率、编码体验和简化使用方面一直在不停的努力中,同时也期待未来有更多的类似 lambda、switch 表达式这样的新特性出来。

1.8.2、新功能和库的更新

1.8.2.1、JEP 358: 改进 NullPointerExceptions 提示信息

Java 14 改进 NullPointerException 的可查性、可读性,能更准确地定位 null 变量的信息。该特性能够帮助开发者和技术支持人员提高生产力,以及改进各种开发工具和调试工具的质量,能够更加准确、清楚地根据动态异常与程序代码相结合来理解程序。

相信每位开发者在实际编码过程中都遇到过 NullPointerException,每当遇到这种异常的时候,都需要根据打印出来的详细信息来分析、定位出现问题的原因,以在程序代码中规避或解决。例如,假设下面代码出现了一个 NullPointerException:

book.id = 99;

打印出来的 NullPointerException 信息如下:

清单 4. NullPointerException 信息

  1. Exception in thread "main" java.lang.NullPointerException
  2. at Book.main(Book.java:5)

像上面这种异常,因为代码比较简单,并且异常信息中也打印出来了行号信息,开发者可以很快速定位到出现异常位置:book 为空而导致的 NullPointerException,而对于一些复杂或者嵌套的情况下出现 NullPointerException 时,仅根据打印出来的信息,很难判断实际出现问题的位置,具体见下面示例:

shoopingcart.buy.book.id = 99;

对于这种比较复杂的情况下,仅仅单根据异常信息中打印的行号,则比较难判断出现 NullPointerException 的原因。

而 Java 14 中,则做了对 NullPointerException 打印异常信息的改进增强,通过分析程序的字节码信息,能够做到准确的定位到出现 NullPointerException 的变量,并且根据实际源代码打印出详细异常信息,对于上述示例,打印信息如下:

清单 5. NullPointerException 详细信息

  1. Exception in thread "main" java.lang.NullPointerException:
  2. Cannot assign field "book" because "shoopingcart.buy" is null
  3. at Book.main(Book.java:5)

对比可以看出,改进之后的 NullPointerException 信息,能够准确打印出具体哪个变量导致的 NullPointerException,减少了由于仅带行号的异常提示信息带来的困惑。该改进功能可以通过如下参数开启:

-XX:+ShowCodeDetailsInExceptionMessages

该增强改进特性,不仅适用于属性访问,还适用于方法调用、数组访问和赋值等有可能会导致 NullPointerException 的地方。

1.8.3、旧功能的删除和弃用

1.8.3.1、JEP 367: 删除 pack200 和 unpack200 工具

删除 pack200 和 unpack200 工具,以及 java.util.jar 包中的 Pack200 API。这些工具和 API 在 Java SE 11 中已被弃用,以便在未来的版本中删除它们。

1.8.4、JVM 相关

1.8.4.1、JEP 345: G1 的 NUMA 可识别内存分配

Java 14 改进非一致性内存访问(NUMA)系统上的 G1 垃圾收集器的整体性能,主要是对年轻代的内存分配进行优化,从而提高 CPU 计算过程中内存访问速度。

NUMA 是 non-unified memory access 的缩写,主要是指在当前的多插槽物理计算机体系中,比较普遍是多核的处理器,并且越来越多的具有 NUMA 内存访问体系结构,即内存与每个插槽或内核之间的距离并不相等。同时套接字之间的内存访问具有不同的性能特征,对更远的套接字的访问通常具有更多的时间消耗。这样每个核对于每一块或者某一区域的内存访问速度会随着核和物理内存所在的位置的远近而有不同的时延差异。

Java 中,堆内存分配一般发生在线程运行的时候,当创建了一个新对象时,该线程会触发 G1 去分配一块内存出来,用来存放新创建的对象,在 G1 内存体系中,其实就是一块 region(大对象除外,大对象需要多个 region),在这个分配新内存的过程中,如果支持了 NUMA 感知内存分配,将会优先在与当前线程所绑定的 NUMA 节点空闲内存区域来执行 allocate 操作,同一线程创建的对象,尽可能的保留在年轻代的同一 NUMA 内存节点上,因为是基于同一个线程创建的对象大部分是短存活并且高概率互相调用的。

具体启用方式可以在 JVM 参数后面加上如下参数:

-XX:+UseNUMA

通过这种方式来启用可识别的内存分配方式,能够提高一些大型计算机的 G1 内存分配回收性能。

1.8.4.2、JEP 363: 删除 CMS 垃圾回收器

CMS 是老年代垃圾回收算法,通过标记-清除的方式进行内存回收,在内存回收过程中能够与用户线程并行执行。CMS 回收器可以与 Serial 回收器和 Parallel New 回收器搭配使用,CMS 主要通过并发的方式,适当减少系统的吞吐量以达到追求响应速度的目的,比较适合在追求 GC 速度的服务器上使用。

因为 CMS 回收算法在进行 GC 回收内存过程中是使用并行方式进行的,如果服务器 CPU 核数不多的情况下,进行 CMS 垃圾回收有可能造成比较高的负载。同时在 CMS 并行标记和并行清理时,应用线程还在继续运行,程序在运行过程中自然会创建新对象、释放不用对象,所以在这个过程中,会有新的不可达内存地址产生,而这部分的不可达内存是出现在标记过程结束之后,本轮 CMS 回收无法在周期内将它们回收掉,只能留在下次垃圾回收周期再清理掉。这样的垃圾就叫做浮动垃圾。由于垃圾收集和用户线程是并发执行的,因此 CMS 回收器不能像其他回收器那样进行内存回收,需要预留一些空间用来保存用户新创建的对象。由于 CMS 回收器在老年代中使用标记-清除的内存回收策略,势必会产生内存碎片,内存当碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有空间但不能再保存对象的情况。

所以,早在几年前的 Java 9 中,就已经决定放弃使用 CMS 回收器了,而这次在 Java 14 中,是继之前 Java 9 中放弃使用 CMS 之后,彻底将其禁用,并删除与 CMS 有关的选项,同时清除与 CMS 有关的文档内容,至此曾经辉煌一度的 CMS 回收器,也将成为历史。

当在 Java 14 版本中,通过使用参数: -XX:+UseConcMarkSweepGC,尝试使用 CMS 时,将会收到下面信息:

  1. Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; \
  2. support was removed in <version>
1.8.4.3、JEP 364&365: ZGC 支持 MacOS 和 Windows 系统(实验阶段)

ZGC 是最初在 Java 11 中引入,同时在后续几个版本中,不断进行改进的一款基于内存 Region,同时使用了内存读屏障、染色指针和内存多重映射等技,并且以可伸缩、低延迟为目标的内存垃圾回收器器,不过在 Java 14 之前版本中,仅仅只支持在 Linux/x64 位平台。

此次 Java 14,同时支持 MacOS 和 Windows 系统,解决了开发人员需要在桌面操作系统中使用 ZGC 的问题。

在 MacOS 和 Windows 下面开启 ZGC 的方式,需要添加如下 JVM 参数:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
1.8.4.4、JEP 366: 弃用 ParallelScavenge 和 SerialOld GC 的组合使用

由于 Parallel Scavenge 和 Serial Old 垃圾收集算法组合起来使用的情况比较少,并且在年轻代中使用并行算法,而在老年代中使用串行算法,这种并行、串行混搭使用的情况,本身已属罕见同时也很冒险。由于这两 GC 算法组合很少使用,却要花费巨大工作量来进行维护,所以在 Java 14 版本中,考虑将这两 GC 的组合弃用。

具体弃用情况如下,通过弃用组合参数:-XX:+UseParallelGC -XX:-UseParallelOldGC,来弃用年轻代、老年期中并行、串行混搭使用的情况;同时,对于单独使用参数:-XX:-UseParallelOldGC 的地方,也将显示该参数已被弃用的警告信息。

1.8.5、新功能的预览和实验

1.8.5.1、JEP 305: instanceof 模式匹配(预览阶段)

Java 14 中对 instanceof 的改进,主要目的是为了让创建对象更简单、简洁和高效,并且可读性更强、提高安全性。

在以往实际使用中,instanceof 主要用来检查对象的类型,然后根据类型对目标对象进行类型转换,之后进行不同的处理、实现不同的逻辑,具体可以参考清单 1:

清单 1. instanceof 传统使用方式

  1. if (person instanceof Student) {
  2. Student student = (Student) person;
  3. student.say();
  4. // other student operations
  5. } else if (person instanceof Teacher) {
  6. Teacher teacher = (Teacher) person;
  7. teacher.say();
  8. // other teacher operations
  9. }

上述代码中,我们首先需要对 person 对象进行类型判断,判断 person 具体是 Student 还是 Teacher,因为这两种角色对应不同操作,亦即对应到的实际逻辑实现,判断完 person 类型之后,然后强制对 person 进行类型转换为局部变量,以方便后续执行属于该角色的特定操作。

上面这种写法,有下面两个问题:

  • 每次在检查类型之后,都需要强制进行类型转换。
  • 类型转换后,需要提前创建一个局部变量来接收转换后的结果,代码显得多余且繁琐。

Java 14 中,对 instanceof 进行模式匹配改进之后,上面示例代码可以改写成:

清单 2. instanceof 模式匹配使用方式

  1. if (person instanceof Student student) {
  2. student.say();
  3. // other student operations
  4. } else if (person instanceof Teacher teacher) {
  5. teacher.say();
  6. // other teacher operations
  7. }

清单 2 中,首先在 if 代码块中,对 person 对象进行类型匹配,校验 person 对象是否为 Student 类型,如果类型匹配成功,则会转换为 Student 类型,并赋值给模式局部变量 student,并且只有当模式匹配表达式匹配成功是才会生效和复制,同时这里的 student 变量只能在 if 块中使用,而不能在 else if/else 中使用,否则会报编译错误。

注意,如果 if 条件中有 && 运算符时,当 instanceof 类型匹配成功,模式局部变量的作用范围也可以相应延长,如下面代码:

清单 3. Instanceof 模式匹配 && 方式

if (obj instanceof String s && s.length() > 5) {.. s.contains(..) ..}

另外,需要注意,这种作用范围延长,并不适用于或 || 运算符,因为即便 || 运算符左边的 instanceof 类型匹配没有成功也不会造成短路,依旧会执行到||运算符右边的表达式,但是此时,因为 instanceof 类型匹配没有成功,局部变量并未定义赋值,此时使用会产生问题。

与传统写法对比,可以发现模式匹配不但提高了程序的安全性、健壮性,另一方面,不需要显式的去进行二次类型转换,减少了大量不必要的强制类型转换。模式匹配变量在模式匹配成功之后,可以直接使用,同时它还被限制了作用范围,大大提高了程序的简洁性、可读性和安全性。instanceof 的模式匹配,为 Java 带来的有一次便捷的提升,能够剔除一些冗余的代码,写出更加简洁安全的代码,提高码代码效率。

1.8.5.2、JEP 359: Record 类型(预览功能)

Java 14 富有建设性地将 Record 类型作为预览特性而引入。Record 类型允许在代码中使用紧凑的语法形式来声明类,而这些类能够作为不可变数据类型的封装持有者。Record 这一特性主要用在特定领域的类上;与枚举类型一样,Record 类型是一种受限形式的类型,主要用于存储、保存数据,并且没有其它额外自定义行为的场景下。

在以往开发过程中,被当作数据载体的类对象,在正确声明定义过程中,通常需要编写大量的无实际业务、重复性质的代码,其中包括:构造函数、属性调用、访问以及 equals() 、hashCode()、toString() 等方法,因此在 Java 14 中引入了 Record 类型,其效果有些类似 Lombok 的 @Data 注解、Kotlin 中的 data class,但是又不尽完全相同,它们的共同点都是类的部分或者全部可以直接在类头中定义、描述,并且这个类只用于存储数据而已。对于 Record 类型,具体可以用下面代码来说明:

清单 6. Record 类型定义

  1. public record Person(String name, int age) {
  2. public static String address;
  3. public String getName() {
  4. return name;
  5. }
  6. }

对上述代码进行编译,然后反编译之后可以看到如下结果:

清单 7. Record 类型反编译结果

  1. public final class Person extends java.lang.Record {
  2. private final java.lang.String name;
  3. private final java.lang.String age;
  4. public Person(java.lang.String name, java.lang.String age) { /* compiled code */ }
  5. public java.lang.String getName() { /* compiled code */ }
  6. public java.lang.String toString() { /* compiled code */ }
  7. public final int hashCode() { /* compiled code */ }
  8. public final boolean equals(java.lang.Object o) { /* compiled code */ }
  9. public java.lang.String name() { /* compiled code */ }
  10. public java.lang.String age() { /* compiled code */ }
  11. }

根据反编译结果,可以得出,当用 Record 来声明一个类时,该类将自动拥有下面特征:

  • 拥有一个构造方法
  • 获取成员属性值的方法:name()、age()
  • hashCode() 方法和 euqals() 方法
  • toString() 方法
  • 类对象和属性被 final 关键字修饰,不能被继承,类的示例属性也都被 final 修饰,不能再被赋值使用。
  • 还可以在 Record 声明的类中定义静态属性、方法和示例方法。注意,不能在 Record 声明的类中定义示例字段,类也不能声明为抽象类等。

可以看到,该预览特性提供了一种更为紧凑的语法来声明类,并且可以大幅减少定义类似数据类型时所需的重复性代码。

另外 Java 14 中为了引入 Record 这种新的类型,在 java.lang.Class 中引入了下面两个新方法:

清单 8. Record 新引入至 Class 中的方法

  1. RecordComponent[] getRecordComponents()
  2. boolean isRecord()

其中 getRecordComponents() 方法返回一组 java.lang.reflect.RecordComponent 对象组成的数组,java.lang.reflect.RecordComponent也是一个新引入类,该数组的元素与 Record 类中的组件相对应,其顺序与在记录声明中出现的顺序相同,可以从该数组中的每个 RecordComponent 中提取到组件信息,包括其名称、类型、泛型类型、注释及其访问方法。

而 isRecord() 方法,则返回所在类是否是 Record 类型,如果是,则返回 true。

1.8.5.3、JEP 368: 文本块(第二预览版本)

Java 13 引入了文本块来解决多行文本的问题,文本块主要以三重双引号开头,并以同样的以三重双引号结尾终止,它们之间的任何内容都被解释为文本块字符串的一部分,包括换行符,避免了对大多数转义序列的需要,并且它仍然是普通的 java.lang.String 对象,文本块可以在 Java 中能够使用字符串的任何地方进行使用,而与编译后的代码没有区别,还增强了 Java 程序中的字符串可读性。并且通过这种方式,可以更直观地表示字符串,可以支持跨越多行,而且不会出现转义的视觉混乱,将可以广泛提高 Java 类程序的可读性和可写性。

Java 14 在 Java 13 引入的文本块的基础之上,新加入了两个转义符,分别是:\ 和 \s,这两个转义符分别表达涵义如下:

  • \:行终止符,主要用于阻止插入换行符;
  • \s:表示一个空格。可以用来避免末尾的白字符被去掉。

在 Java 13 之前,多行字符串写法为:

清单 11. 多行字符串写法

  1. String literal = "Lorem ipsum dolor sit amet, consectetur adipiscing " +
  2. "elit, sed do eiusmod tempor incididunt ut labore " +
  3. "et dolore magna aliqua.";

在 Java 14 新引入两个转义符之后,上述内容可以写为:

清单 12. 多行文本块加上转义符的写法

  1. String text = """
  2. Lorem ipsum dolor sit amet, consectetur adipiscing \
  3. elit, sed do eiusmod tempor incididunt ut labore \
  4. et dolore magna aliqua.\
  5. """;

上述两种写法,text 实际还是只有一行内容。

对于转义符:\s,用法如下,能够保证下列文本每行正好都是六个字符长度:

清单 13. 多行文本块加上转义符的写法

  1. String colors = """
  2. red \s
  3. green\s
  4. blue \s
  5. """;

Java 14 带来的这两个转义符,能够简化跨多行字符串编码问题,通过转义符,能够避免对换行等特殊字符串进行转移,从而简化代码编写,同时也增强了使用 String 来表达 HTML、XML、SQL 或 JSON 等格式字符串的编码可读性,且易于维护。

同时 Java 14 还对 String 进行了方法扩展:

  • stripIndent() :用于从文本块中去除空白字符
  • translateEscapes():用于翻译转义字符
  • formatted(Object... args):用于格式化
1.8.5.4、JEP 343: 打包工具(孵化器版本)

创建用于打包自包含 Java 应用程序的工具。

它基于 JavaFX javapackager 工具创建一个简单的打包工具,主要目标是:

  • 支持原生打包格式,为最终用户提供自然的安装体验。这些格式包括 Windows 上的 msi 和 exe,macOS 上的 pkg 和 dmg,以及 Linux 上的 deb 和 rpm。
  • 允许在打包时指定启动时间参数。
  • 可以从命令行直接调用,也可以通过 ToolProvider API 以编程方式调用。
1.8.5.5、JEP 370: 外部存储器访问 API(孵化器版)

外存访问 API(二次孵化),可以允许 Java 应用程序安全有效地访问 Java 堆之外的外部内存。目的是引入一个 API,以允许 Java 程序安全、有效地访问 Java 堆之外的外部存储器。如本机、持久和托管堆。如下内容来源于https://xie.infoq.cn/article/8304c894c4e38318d38ceb116,作者是九叔

在实际的开发过程中,绝大多数的开发人员基本都不会直接与堆外内存打交道,但这并不代表你从未接触过堆外内存,像大家经常使用的诸如:RocketMQ、MapDB 等中间件产品底层实现都是基于堆外存储的,换句话说,我们几乎每天都在间接与堆外内存打交道。那么究竟为什么需要使用到堆外内存呢?简单来说,主要是出于以下 3 个方面的考虑:

  • 减少 GC 次数和降低 Stop-the-world 时间;
  • 可以扩展和使用更大的内存空间;
  • 可以省去物理内存和堆内存之间的数据复制步骤。

在 Java14 之前,如果开发人员想要操作堆外内存,通常的做法就是使用 ByteBuffer 或者 Unsafe,甚至是 JNI 等方式,但无论使用哪一种方式,均无法同时有效解决安全性和高效性等 2 个问题,并且,堆外内存的释放也是一个令人头痛的问题。以 DirectByteBuffer 为例,该对象仅仅只是一个引用,其背后还关联着一大段堆外内存,由于 DirectByteBuffer 对象实例仍然是存储在堆空间内,只有当 DirectByteBuffer 对象被 GC 回收时,其背后的堆外内存才会被进一步释放。

在此大家需要注意,程序中通过 ByteBuffer.allocateDirect()方法来申请物理内存资源所耗费的成本远远高于直接在 on-heap 中的操作,而且实际开发过程中还需要考虑数据结构如何设计、序列化/反序列化如何支撑等诸多难题,所以与其使用语法层面的 API 倒不如直接使用 MapDB 等开源产品来得更实惠。

如今,在堆外内存领域,我们似乎又多了一个选择,从 Java14 开始,Java 的设计者们在语法层面为大家带来了崭新的 Memory Access API,极大程度上简化了开发难度,并得以有效的解决了安全性和高效性等 2 个核心问题。示例:

  1. // 获取内存访问var句柄
  2. var handle = MemoryHandles.varHandle(char.class,
  3. ByteOrder.nativeOrder());
  4. // 申请200字节的堆外内存
  5. try (MemorySegment segment = MemorySegment.allocateNative(200)) {
  6. for (int i = 0; i < 25; i++) {
  7. handle.set(segment, i << 2, (char) (i + 1 + 64));
  8. System.out.println(handle.get(segment, i << 2));
  9. }
  10. }

关于堆外内存段的释放,Memory Access API 提供有显式和隐式 2 种方式,开发人员除了可以在程序中通过 MemorySegment 的 close()方法来显式释放所申请的内存资源外,还可以注册 Cleaner 清理器来实现资源的隐式释放,后者会在 GC 确定目标内存段不再可访问时,释放与之关联的堆外内存资源。

1.8.6、结束语

Java 在更新版本周期为每半年发布一次之后,目前来看,确实是严格保持每半年更新的节奏。Java 14 版本的发布带来了不少新特性、功能实用性的增强、性能提升和 GC 方面的改进尝试。本文仅针对其中对使用人员影响较大的以及其中主要的特性做了介绍,如有兴趣,您还可以自行下载相关代码,继续深入研究。

1.9、Java 15 新特性概述

JDK 15 在 2020 年 9 月 15 号正式发布了!根据发布的规划,这次发布的 JDK 15 将是一个短期的过度版,只会被 Oracle 支持(维护)6 个月,直到明年 3 月的 JDK 16 发布此版本将停止维护。而 Oracle 下一个长期支持版(LTS 版)会在明年的 9 月份候发布(Java 17),LTS 版每 3 年发布一个,上一次长期支持版是 18 年 9 月发布的 JDK 11。

知识体系系统性梳理

1.9.1、语言特性增强

1.9.1.1、JEP 378: 文本块(Text Blocks)

文本块,是一个多行字符串,它可以避免使用大多数转义符号,自动以可预测的方式格式化字符串,并让开发人员在需要时可以控制格式。

Text Blocks首次是在JDK 13中以预览功能出现的,然后在JDK 14中又预览了一次,终于在JDK 15中被确定下来,可放心使用了。

  1. public static void main(String[] args) {
  2. String query = """
  3. SELECT * from USER \
  4. WHERE `id` = 1 \
  5. ORDER BY `id`, `name`;\
  6. """;
  7. System.out.println(query);
  8. }

运行程序,输出(可以看到展示为一行了):

SELECT * from USER WHERE `id` = 1 ORDER BY `id`, `name`;

1.9.2、新功能和库的更新

1.9.2.1、JEP 339: Edwards-Curve 数字签名算法 (EdDSA)

Edwards-Curve 数字签名算法(EdDSA),一种根据 RFC 8032 规范所描述的 Edwards-Curve 数字签名算法(EdDSA)实现加密签名,实现了一种 RFC 8032 标准化方案,但它不能代替 ECDSA。

与 JDK 中的现有签名方案相比,EdDSA 具有更高的安全性和性能,因此备受关注。它已经在OpenSSL和BoringSSL等加密库中得到支持,在区块链领域用的比较多。

EdDSA是一种现代的椭圆曲线方案,具有JDK中现有签名方案的优点。EdDSA将只在SunEC提供商中实现。

  1. // example: generate a key pair and sign
  2. KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519");
  3. KeyPair kp = kpg.generateKeyPair();
  4. // algorithm is pure Ed25519
  5. Signature sig = Signature.getInstance("Ed25519");
  6. sig.initSign(kp.getPrivate());
  7. sig.update(msg);
  8. byte[] s = sig.sign();
  9. // example: use KeyFactory to contruct a public key
  10. KeyFactory kf = KeyFactory.getInstance("EdDSA");
  11. boolean xOdd = ...
  12. BigInteger y = ...
  13. NamedParameterSpec paramSpec = new NamedParameterSpec("Ed25519");
  14. EdECPublicKeySpec pubSpec = new EdECPublicKeySpec(paramSpec, new EdPoint(xOdd, y));
  15. PublicKey pubKey = kf.generatePublic(pubSpec);
1.9.2.2、JEP 371: 隐藏类 Hidden Classes

隐藏类是为框架(frameworks)所设计的,隐藏类不能直接被其他类的字节码使用,只能在运行时生成类并通过反射间接使用它们。

该提案通过启用标准 API 来定义 无法发现 且 具有有限生命周期 的隐藏类,从而提高 JVM 上所有语言的效率。JDK内部和外部的框架将能够动态生成类,而这些类可以定义隐藏类。通常来说基于JVM的很多语言都有动态生成类的机制,这样可以提高语言的灵活性和效率。

  • 隐藏类天生为框架设计的,在运行时生成内部的class。
  • 隐藏类只能通过反射访问,不能直接被其他类的字节码访问。
  • 隐藏类可以独立于其他类加载、卸载,这可以减少框架的内存占用。

Hidden Classes是什么呢

Hidden Classes就是不能直接被其他class的二进制代码使用的class。Hidden Classes主要被一些框架用来生成运行时类,但是这些类不是被用来直接使用的,而是通过反射机制来调用。

比如在JDK8中引入的lambda表达式,JVM并不会在编译的时候将lambda表达式转换成为专门的类,而是在运行时将相应的字节码动态生成相应的类对象。

另外使用动态代理也可以为某些类生成新的动态类。

那么我们希望这些动态生成的类需要具有什么特性呢

  • 不可发现性。 因为我们是为某些静态的类动态生成的动态类,所以我们希望把这个动态生成的类看做是静态类的一部分。所以我们不希望除了该静态类之外的其他机制发现。
  • 访问控制。 我们希望在访问控制静态类的同时,也能控制到动态生成的类。
  • 生命周期。 动态生成类的生命周期一般都比较短,我们并不需要将其保存和静态类的生命周期一致。

API的支持

所以我们需要一些API来定义无法发现的且具有有限生命周期的隐藏类。这将提高所有基于JVM的语言实现的效率。

比如:

  1. java.lang.reflect.Proxy // 可以定义隐藏类作为实现代理接口的代理类。
  2. java.lang.invoke.StringConcatFactory // 可以生成隐藏类来保存常量连接方法;
  3. java.lang.invoke.LambdaMetaFactory //可以生成隐藏的nestmate类,以容纳访问封闭变量的lambda主体;

普通类是通过调用ClassLoader::defineClass创建的,而隐藏类是通过调用Lookup::defineHiddenClass创建的。这使JVM从提供的字节中派生一个隐藏类,链接该隐藏类,并返回提供对隐藏类的反射访问的查找对象。调用程序可以通过返回的查找对象来获取隐藏类的Class对象。

1.9.2.3、JEP 373: 重新实现 DatagramSocket API

重新实现了老的 DatagramSocket API 接口,更改了 java.net.DatagramSocket 和 java.net.MulticastSocket 为更加简单、现代化的底层实现,更易于维护和调试。

java.net.datagram.Socketjava.net.MulticastSocket的当前实现可以追溯到JDK 1.0,那时IPv6还在开发中。因此,当前的多播套接字实现尝试调和IPv4和IPv6难以维护的方式。

  • 通过替换 java.net.datagram 的基础实现,重新实现旧版 DatagramSocket API。
  • 更改java.net.DatagramSocketjava.net.MulticastSocket 为更加简单、现代化的底层实现。提高了 JDK 的可维护性和稳定性。
  • 通过将java.net.datagram.Socketjava.net.MulticastSocket API的底层实现替换为更简单、更现代的实现来重新实现遗留的DatagramSocket API。

新的实现

  • 易于调试和维护;
  • 与Project Loom中正在探索的虚拟线程协同。

1.9.3、JVM 优化

1.9.3.1、JEP 373: ZGC: 可伸缩低延迟垃圾收集器

ZGC是Java 11引入的新的垃圾收集器(JDK9以后默认的垃圾回收器是G1),经过了多个实验阶段,自此终于成为正式特性。ZGC是一个重新设计的并发的垃圾回收器,可以极大的提升GC的性能。支持任意堆大小而保持稳定的低延迟(10ms以内),性能非常可观。目前默认垃圾回收器仍然是 G1,后续很有可以能将ZGC设为默认垃圾回收器。之前需要通过-XX:+UnlockExperimentalVMOptions -XX:+UseZGC来启用ZGC,现在只需要-XX:+UseZGC就可以。

以下是相关介绍:

ZGC 是一个可伸缩的、低延迟的垃圾收集器,主要为了满足如下目标进行设计:

  • GC 停顿时间不超过 10ms
  • 即能处理几百 MB 的小堆,也能处理几个 TB 的大堆
  • 应用吞吐能力不会下降超过 15%(与 G1 回收算法相比)
  • 方便在此基础上引入新的 GC 特性和利用 colord
  • 针以及 Load barriers 优化奠定基础
  • 当前只支持 Linux/x64 位平台 停顿时间在 10ms 以下,10ms 其实是一个很保守的数据,即便是 10ms 这个数据,也是 GC 调优几乎达不到的极值。根据 SPECjbb 2015 的基准测试,128G 的大堆下最大停顿时间才 1.68ms,远低于 10ms,和 G1 算法相比,改进非常明显。

本图片引用自: The Z Garbage Collector – An Introduction

不过目前 ZGC 还处于实验阶段,目前只在 Linux/x64 上可用,如果有足够的需求,将来可能会增加对其他平台的支持。同时作为实验性功能的 ZGC 将不会出现在 JDK 构建中,除非在编译时使用 configure 参数: --with-jvm-features=zgc 显式启用。

在实验阶段,编译完成之后,已经迫不及待的想试试 ZGC,需要配置以下 JVM 参数,才能使用 ZGC,具体启动 ZGC 参数如下:

-XX:+ UnlockExperimentalVMOptions -XX:+ UseZGC -Xmx10g

其中参数: -Xmx 是 ZGC 收集器中最重要的调优选项,大大解决了程序员在 JVM 参数调优上的困扰。ZGC 是一个并发收集器,必须要设置一个最大堆的大小,应用需要多大的堆,主要有下面几个考量:

  • 对象的分配速率,要保证在 GC 的时候,堆中有足够的内存分配新对象。
  • 一般来说,给 ZGC 的内存越多越好,但是也不能浪费内存,所以要找到一个平衡。
1.9.3.2、JEP 374: 禁用偏向锁定

准备禁用和废除偏向锁,在 JDK 15 中,默认情况下禁用偏向锁,并弃用所有相关的命令行选项。

在默认情况下禁用偏向锁定,并弃用所有相关命令行选项。目标是确定是否需要继续支持偏置锁定的 高维护成本 的遗留同步优化, HotSpot虚拟机使用该优化来减少非竞争锁定的开销。 尽管某些Java应用程序在禁用偏向锁后可能会出现性能下降,但偏向锁的性能提高通常不像以前那么明显。

该特性默认禁用了biased locking(-XX:+UseBiasedLocking),并且废弃了所有相关的命令行选型(BiasedLockingStartupDelay, BiasedLockingBulkRebiasThreshold, BiasedLockingBulkRevokeThreshold, BiasedLockingDecayTime, UseOptoBiasInlining, PrintBiasedLockingStatistics and PrintPreciseBiasedLockingStatistics)

1.9.3.3、JEP 379: Shenandoah:低暂停时间垃圾收集器(转正)

Shenandoah垃圾回收算法终于从实验特性转变为产品特性,这是一个从 JDK 12 引入的回收算法,该算法通过与正在运行的 Java 线程同时进行疏散工作来减少 GC 暂停时间。Shenandoah 的暂停时间与堆大小无关,无论堆栈是 200 MB 还是 200 GB,都具有相同的一致暂停时间。

Shenandoah适用于高吞吐和大内存场景,不适合高实时性场景。Shenandoah算法设计目标主要是响应性和一致可控的短暂停顿,对于垃圾回收生命周期中安全点停顿(TTSP)和内存增长监控的时间开销并无帮助。

Shenandoah算法为每个Java对象添加了一个间接指针,使得GC线程能够在Java线程运行时压缩堆。标记和压缩是同时执行的,因此我们只需要暂停Java线程在一致可控的时间内扫描线程堆栈以查找和更新对象图的根。

怎么形容Shenandoah和ZGC的关系呢?异同点大概如下:

  • 相同点:性能几乎可认为是相同的
  • 不同点:ZGC是Oracle JDK的。而Shenandoah只存在于OpenJDK中,因此使用时需注意你的JDK版本
  • 打开方式:使用-XX:+UseShenandoahGC命令行参数打开。

Shenandoah在JDK12被作为experimental引入,在JDK15变为Production;之前需要通过-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC来启用,现在只需要-XX:+UseShenandoahGC即可启用

1.9.4、旧功能的删除和弃用

1.9.4.1、JEP 372: 移除Nashorn JavaScript引擎

移除了 Nashorn JavaScript 脚本引擎、APIs,以及 jjs 工具。这些早在 JDK 11 中就已经被标记为 deprecated 了,JDK 15 被移除就很正常了。

Nashorn引擎是什么

Nashorn 是 JDK 1.8 引入的一个 JavaScript 脚本引擎,用来取代 Rhino 脚本引擎。Nashorn 是 ECMAScript-262 5.1 的完整实现,增强了 Java 和 JavaScript 的兼容性,并且大大提升了性能。

为什么要移除

官方的描述是,随着 ECMAScript 脚本语言的结构、API 的改编速度越来越快,维护 Nashorn 太有挑战性了,所以……。

1.9.4.2、JEP 381: 移除了 Solaris 和 SPARC 端口。

移除了 Solaris/SPARC、Solaris/x64 和 Linux/SPARC 端口的源代码及构建支持。这些端口在 JDK 14 中就已经被标记为 deprecated 了,JDK 15 被移除也不奇怪。

删除对Solaris/SPARC、Solaris/x64和Linux/SPARC端口的源代码和构建支持,在JDK 14中被标记为废弃,在JDK15版本正式移除。 许多正在开发的项目和功能(如Valhalla、Loom和Panama)需要进行重大更改以适应CPU架构和操作系统特定代码。

近年来,Solaris 和 SPARC 都已被 Linux 操作系统和英特尔处理器取代。放弃对 Solaris 和 SPARC 端口的支持将使 OpenJDK 社区的贡献者能够加速开发新功能,从而推动平台向前发展。

1.9.4.3、JEP 385: 废除 RMI 激活

RMI Activation被标记为Deprecate,将会在未来的版本中删除。RMI激活机制是RMI中一个过时的部分,自Java 8以来一直是可选的而非必选项。RMI激活机制增加了持续的维护负担。RMI的其他部分暂时不会被弃用。

RMI jdk1.2引入,EJB在RMI系统中,我们使用延迟激活。延迟激活将激活对象推迟到客户第一次使用(即第一次方法调用)之前。 既然RMI Activation这么好用,为什么要废弃呢?

因为对于现代应用程序来说,分布式系统大部分都是基于Web的,web服务器已经解决了穿越防火墙,过滤请求,身份验证和安全性的问题,并且也提供了很多延迟加载的技术。

所以在现代应用程序中,RMI Activation已经很少被使用到了。并且在各种开源的代码库中,也基本上找不到RMI Activation的使用代码了。 为了减少RMI Activation的维护成本,在JDK8中,RMI Activation被置为可选的。现在在JDK15中,终于可以废弃了。

1.9.5、新功能的预览和孵化

1.9.5.1、JEP 375: instanceof 自动匹配模式(第二次预览)

模式匹配(第二次预览),第一次预览是 JDK 14 中提出来的。

Java 14 之前:

  1. if (object instanceof Kid) {
  2. Kid kid = (Kid) object;
  3. // ...
  4. } else if (object instanceof Kiddle) {
  5. Kid kid = (Kid) object;
  6. // ...
  7. }

Java 14+:

  1. if (object instanceof Kid kid) {
  2. // ...
  3. } else if (object instanceof Kiddle kiddle) {
  4. // ...
  5. }

Java 15 并没有对此特性进行调整,继续预览特性,只是为了收集更多的用户反馈,可能还不成熟吧。

1.9.5.2、JEP 360: 密封的类和接口(预览)

封闭类(预览特性),可以是封闭类和或者封闭接口,用来增强 Java 编程语言,防止其他类或接口扩展或实现它们

因为我们引入了sealed classinterfaces,这些class或者interfaces只允许被指定的类或者interface进行扩展和实现。

使用修饰符sealed,您可以将一个类声明为密封类。密封的类使用reserved关键字permits列出可以直接扩展它的类。子类可以是最终的,非密封的或密封的。

之前我们的代码是这样的。

  1. public class Person { } //人
  2. class Teacher extends Person { }//教师
  3. class Worker extends Person { } //工人
  4. class Student extends Person{ } //学生

但是我们现在要限制 Person类 只能被这三个类继承,不能被其他类继承,需要这么做。

  1. // 添加sealed修饰符,permits后面跟上只能被继承的子类名称
  2. public sealed class Person permits Teacher, Worker, Student{ } //人
  3. // 子类可以被修饰为 final
  4. final class Teacher extends Person { }//教师
  5. // 子类可以被修饰为 non-sealed,此时 Worker类就成了普通类,谁都可以继承它
  6. non-sealed class Worker extends Person { } //工人
  7. // 任何类都可以继承Worker
  8. class AnyClass extends Worker{}
  9. //子类可以被修饰为 sealed,同上
  10. sealed class Student extends Person permits MiddleSchoolStudent,GraduateStudent{ } //学生
  11. final class MiddleSchoolStudent extends Student { } //中学生
  12. final class GraduateStudent extends Student { } //研究生

很强很实用的一个特性,可以限制类的层次结构。

1.9.5.3、JEP 383: 外部存储器访问 API(二次孵化器版)

外存访问 API(二次孵化),可以允许 Java 应用程序安全有效地访问 Java 堆之外的外部内存。目的是引入一个 API,以允许 Java 程序安全、有效地访问 Java 堆之外的外部存储器。如本机、持久和托管堆。如下内容来源于https://xie.infoq.cn/article/8304c894c4e38318d38ceb116,作者是九叔

在实际的开发过程中,绝大多数的开发人员基本都不会直接与堆外内存打交道,但这并不代表你从未接触过堆外内存,像大家经常使用的诸如:RocketMQ、MapDB 等中间件产品底层实现都是基于堆外存储的,换句话说,我们几乎每天都在间接与堆外内存打交道。那么究竟为什么需要使用到堆外内存呢?简单来说,主要是出于以下 3 个方面的考虑:

  • 减少 GC 次数和降低 Stop-the-world 时间;
  • 可以扩展和使用更大的内存空间;
  • 可以省去物理内存和堆内存之间的数据复制步骤。

在 Java14 之前,如果开发人员想要操作堆外内存,通常的做法就是使用 ByteBuffer 或者 Unsafe,甚至是 JNI 等方式,但无论使用哪一种方式,均无法同时有效解决安全性和高效性等 2 个问题,并且,堆外内存的释放也是一个令人头痛的问题。以 DirectByteBuffer 为例,该对象仅仅只是一个引用,其背后还关联着一大段堆外内存,由于 DirectByteBuffer 对象实例仍然是存储在堆空间内,只有当 DirectByteBuffer 对象被 GC 回收时,其背后的堆外内存才会被进一步释放。

在此大家需要注意,程序中通过 ByteBuffer.allocateDirect()方法来申请物理内存资源所耗费的成本远远高于直接在 on-heap 中的操作,而且实际开发过程中还需要考虑数据结构如何设计、序列化/反序列化如何支撑等诸多难题,所以与其使用语法层面的 API 倒不如直接使用 MapDB 等开源产品来得更实惠。

如今,在堆外内存领域,我们似乎又多了一个选择,从 Java14 开始,Java 的设计者们在语法层面为大家带来了崭新的 Memory Access API,极大程度上简化了开发难度,并得以有效的解决了安全性和高效性等 2 个核心问题。示例:

  1. // 获取内存访问var句柄
  2. var handle = MemoryHandles.varHandle(char.class,
  3. ByteOrder.nativeOrder());
  4. // 申请200字节的堆外内存
  5. try (MemorySegment segment = MemorySegment.allocateNative(200)) {
  6. for (int i = 0; i < 25; i++) {
  7. handle.set(segment, i << 2, (char) (i + 1 + 64));
  8. System.out.println(handle.get(segment, i << 2));
  9. }
  10. }

关于堆外内存段的释放,Memory Access API 提供有显式和隐式 2 种方式,开发人员除了可以在程序中通过 MemorySegment 的 close()方法来显式释放所申请的内存资源外,还可以注册 Cleaner 清理器来实现资源的隐式释放,后者会在 GC 确定目标内存段不再可访问时,释放与之关联的堆外内存资源。

1.9.5.4、JEP 384: Records (二次预览)

Records 最早在 JDK 14 中成为预览特性,JDK 15 继续二次预览。

如下内容来自Java14

Record 类型允许在代码中使用紧凑的语法形式来声明类,而这些类能够作为不可变数据类型的封装持有者。Record 这一特性主要用在特定领域的类上;与枚举类型一样,Record 类型是一种受限形式的类型,主要用于存储、保存数据,并且没有其它额外自定义行为的场景下。

在以往开发过程中,被当作数据载体的类对象,在正确声明定义过程中,通常需要编写大量的无实际业务、重复性质的代码,其中包括:构造函数、属性调用、访问以及 equals() 、hashCode()、toString() 等方法,因此在 Java 14 中引入了 Record 类型,其效果有些类似 Lombok 的 @Data 注解、Kotlin 中的 data class,但是又不尽完全相同,它们的共同点都是类的部分或者全部可以直接在类头中定义、描述,并且这个类只用于存储数据而已。对于 Record 类型,具体可以用下面代码来说明:

  1. public record Person(String name, int age) {
  2. public static String address;
  3. public String getName() {
  4. return name;
  5. }
  6. }

对上述代码进行编译,然后反编译之后可以看到如下结果:

  1. public final class Person extends java.lang.Record {
  2. private final java.lang.String name;
  3. private final java.lang.String age;
  4. public Person(java.lang.String name, java.lang.String age) { /* compiled code */ }
  5. public java.lang.String getName() { /* compiled code */ }
  6. public java.lang.String toString() { /* compiled code */ }
  7. public final int hashCode() { /* compiled code */ }
  8. public final boolean equals(java.lang.Object o) { /* compiled code */ }
  9. public java.lang.String name() { /* compiled code */ }
  10. public java.lang.String age() { /* compiled code */ }
  11. }

根据反编译结果,可以得出,当用 Record 来声明一个类时,该类将自动拥有下面特征:

  • 拥有一个构造方法
  • 获取成员属性值的方法:name()、age()
  • hashCode() 方法和 euqals() 方法
  • toString() 方法
  • 类对象和属性被 final 关键字修饰,不能被继承,类的示例属性也都被 final 修饰,不能再被赋值使用。
  • 还可以在 Record 声明的类中定义静态属性、方法和示例方法。注意,不能在 Record 声明的类中定义示例字段,类也不能声明为抽象类等。

可以看到,该预览特性提供了一种更为紧凑的语法来声明类,并且可以大幅减少定义类似数据类型时所需的重复性代码。

另外 Java 14 中为了引入 Record 这种新的类型,在 java.lang.Class 中引入了下面两个新方法:

  1. RecordComponent[] getRecordComponents()
  2. boolean isRecord()

其中 getRecordComponents() 方法返回一组 java.lang.reflect.RecordComponent 对象组成的数组,java.lang.reflect.RecordComponent也是一个新引入类,该数组的元素与 Record 类中的组件相对应,其顺序与在记录声明中出现的顺序相同,可以从该数组中的每个 RecordComponent 中提取到组件信息,包括其名称、类型、泛型类型、注释及其访问方法。

而 isRecord() 方法,则返回所在类是否是 Record 类型,如果是,则返回 true。

1.10、Java 16 新特性概述

JDK 16 在 2021 年 3 月 16 号发布!根据发布的规划,这次发布的 JDK 17 是一个长期维护的版本(LTS)。Java 16 提供了数千个性能稳定性安全性更新,以及 17 个 JEP(JDK 增强提案),进一步改进了 Java 语言和平台,以帮助开发人员提高工作效率。

知识体系系统性梳理

1.10.1、语言特性增强

1.10.1.1、JEP 394: instanceof 模式匹配(正式版)

模式匹配(Pattern Matching)最早在 Java 14 中作为预览特性引入,在 Java 15 中还是预览特性,在Java 16中成为正式版。模式匹配通过对 instacneof 运算符进行模式匹配来增强 Java 编程语言。

如下内容来自Java14

对 instanceof 的改进,主要目的是为了让创建对象更简单、简洁和高效,并且可读性更强、提高安全性。

在以往实际使用中,instanceof 主要用来检查对象的类型,然后根据类型对目标对象进行类型转换,之后进行不同的处理、实现不同的逻辑,具体可以参考如下:

  1. if (person instanceof Student) {
  2. Student student = (Student) person;
  3. student.say();
  4. // other student operations
  5. } else if (person instanceof Teacher) {
  6. Teacher teacher = (Teacher) person;
  7. teacher.say();
  8. // other teacher operations
  9. }

上述代码中,我们首先需要对 person 对象进行类型判断,判断 person 具体是 Student 还是 Teacher,因为这两种角色对应不同操作,亦即对应到的实际逻辑实现,判断完 person 类型之后,然后强制对 person 进行类型转换为局部变量,以方便后续执行属于该角色的特定操作。

上面这种写法,有下面两个问题:

  • 每次在检查类型之后,都需要强制进行类型转换。
  • 类型转换后,需要提前创建一个局部变量来接收转换后的结果,代码显得多余且繁琐。

对 instanceof 进行模式匹配改进之后,上面示例代码可以改写成:

  1. if (person instanceof Student student) {
  2. student.say();
  3. // other student operations
  4. } else if (person instanceof Teacher teacher) {
  5. teacher.say();
  6. // other teacher operations
  7. }

首先在 if 代码块中,对 person 对象进行类型匹配,校验 person 对象是否为 Student 类型,如果类型匹配成功,则会转换为 Student 类型,并赋值给模式局部变量 student,并且只有当模式匹配表达式匹配成功是才会生效和复制,同时这里的 student 变量只能在 if 块中使用,而不能在 else if/else 中使用,否则会报编译错误。

注意,如果 if 条件中有 && 运算符时,当 instanceof 类型匹配成功,模式局部变量的作用范围也可以相应延长,如下面代码:

if (obj instanceof String s && s.length() > 5) {.. s.contains(..) ..}

另外,需要注意,这种作用范围延长,并不适用于或 || 运算符,因为即便 || 运算符左边的 instanceof 类型匹配没有成功也不会造成短路,依旧会执行到||运算符右边的表达式,但是此时,因为 instanceof 类型匹配没有成功,局部变量并未定义赋值,此时使用会产生问题。

与传统写法对比,可以发现模式匹配不但提高了程序的安全性、健壮性,另一方面,不需要显式的去进行二次类型转换,减少了大量不必要的强制类型转换。模式匹配变量在模式匹配成功之后,可以直接使用,同时它还被限制了作用范围,大大提高了程序的简洁性、可读性和安全性。instanceof 的模式匹配,为 Java 带来的有一次便捷的提升,能够剔除一些冗余的代码,写出更加简洁安全的代码,提高码代码效率。

1.10.1.2、JEP 395: Records (正式版)

Records 最早在 Java 14 中作为预览特性引入,在 Java 15 中还是预览特性,在Java 16中成为正式版。

如下内容来自Java14

Record 类型允许在代码中使用紧凑的语法形式来声明类,而这些类能够作为不可变数据类型的封装持有者。Record 这一特性主要用在特定领域的类上;与枚举类型一样,Record 类型是一种受限形式的类型,主要用于存储、保存数据,并且没有其它额外自定义行为的场景下。

在以往开发过程中,被当作数据载体的类对象,在正确声明定义过程中,通常需要编写大量的无实际业务、重复性质的代码,其中包括:构造函数、属性调用、访问以及 equals() 、hashCode()、toString() 等方法,因此在 Java 14 中引入了 Record 类型,其效果有些类似 Lombok 的 @Data 注解、Kotlin 中的 data class,但是又不尽完全相同,它们的共同点都是类的部分或者全部可以直接在类头中定义、描述,并且这个类只用于存储数据而已。对于 Record 类型,具体可以用下面代码来说明:

  1. public record Person(String name, int age) {
  2. public static String address;
  3. public String getName() {
  4. return name;
  5. }
  6. }

对上述代码进行编译,然后反编译之后可以看到如下结果:

  1. public final class Person extends java.lang.Record {
  2. private final java.lang.String name;
  3. private final java.lang.String age;
  4. public Person(java.lang.String name, java.lang.String age) { /* compiled code */ }
  5. public java.lang.String getName() { /* compiled code */ }
  6. public java.lang.String toString() { /* compiled code */ }
  7. public final int hashCode() { /* compiled code */ }
  8. public final boolean equals(java.lang.Object o) { /* compiled code */ }
  9. public java.lang.String name() { /* compiled code */ }
  10. public java.lang.String age() { /* compiled code */ }
  11. }

根据反编译结果,可以得出,当用 Record 来声明一个类时,该类将自动拥有下面特征:

  • 拥有一个构造方法
  • 获取成员属性值的方法:name()、age()
  • hashCode() 方法和 euqals() 方法
  • toString() 方法
  • 类对象和属性被 final 关键字修饰,不能被继承,类的示例属性也都被 final 修饰,不能再被赋值使用。
  • 还可以在 Record 声明的类中定义静态属性、方法和示例方法。注意,不能在 Record 声明的类中定义示例字段,类也不能声明为抽象类等。

可以看到,该预览特性提供了一种更为紧凑的语法来声明类,并且可以大幅减少定义类似数据类型时所需的重复性代码。

另外 Java 14 中为了引入 Record 这种新的类型,在 java.lang.Class 中引入了下面两个新方法:

  1. RecordComponent[] getRecordComponents()
  2. boolean isRecord()

其中 getRecordComponents() 方法返回一组 java.lang.reflect.RecordComponent 对象组成的数组,java.lang.reflect.RecordComponent也是一个新引入类,该数组的元素与 Record 类中的组件相对应,其顺序与在记录声明中出现的顺序相同,可以从该数组中的每个 RecordComponent 中提取到组件信息,包括其名称、类型、泛型类型、注释及其访问方法。

而 isRecord() 方法,则返回所在类是否是 Record 类型,如果是,则返回 true。

1.10.2、新工具和库

1.10.2.1、JEP 380:Unix-Domain 套接字通道

Unix-domain 套接字一直是大多数 Unix 平台的一个特性,现在在 Windows 10 和 Windows Server 2019 也提供了支持。此特性为 java.nio.channels 包的套接字通道和服务器套接字通道 API 添加了 Unix-domain(AF_UNIX)套接字支持。它扩展了继承的通道机制以支持 Unix-domain 套接字通道和服务器套接字通道。Unix-domain 套接字用于同一主机上的进程间通信(IPC)。它们在很大程度上类似于 TCP/IP,区别在于套接字是通过文件系统路径名而不是 Internet 协议(IP)地址和端口号寻址的。对于本地进程间通信,Unix-domain 套接字比 TCP/IP 环回连接更安全、更有效。

1.10.2.2、JEP 390: 对基于值的类发出警告

JDK9注解@Deprecated得到了增强,增加了 since 和 forRemoval 两个属性,可以分别指定一个程序元素被废弃的版本,以及是否会在今后的版本中被删除。JDK16中对@jdk.internal.ValueBased注解加入了基于值的类的告警,所以继续在 Synchronized 同步块中使用值类型,将会在编译期和运行期产生警告,甚至是异常。

  • JDK9中@Deprecated增强了增加了 since 和 forRemoval 两 个属性

JDK9注解@Deprecated得到了增强,增加了 since 和 forRemoval 两个属性,可以分别指定一个程序元素被废弃的版本,以及是否会在今后的版本中被删除。

在如下的代码中,表示PdaiDeprecatedTest这个类在JDK9版本中被弃用并且在将来的某个版本中一定会被删除。

  1. @Deprecated(since="9", forRemoval = true)
  2. public class PdaiDeprecatedTest {
  3. }
  • JDK16中对基于值的类(@jdk.internal.ValueBased)给出告警

在JDK9中我们可以看到Integer.java类构造函数中加入了@Deprecated(since="9"),表示在JDK9版本中被弃用并且在将来的某个版本中一定会被删除

  1. public final class Integer extends Number implements Comparable<Integer> {
  2. // ...
  3. /**
  4. * Constructs a newly allocated {@code Integer} object that
  5. * represents the specified {@code int} value.
  6. *
  7. * @param value the value to be represented by the
  8. * {@code Integer} object.
  9. *
  10. * @deprecated
  11. * It is rarely appropriate to use this constructor. The static factory
  12. * {@link #valueOf(int)} is generally a better choice, as it is
  13. * likely to yield significantly better space and time performance.
  14. */
  15. @Deprecated(since="9")
  16. public Integer(int value) {
  17. this.value = value;
  18. }
  19. // ...
  20. }

如下是JDK16中Integer.java的代码

  1. /*
  2. * <p>This is a <a href="{@docRoot}/java.base/java/lang/doc-files/ValueBased.html">value-based</a>
  3. * class; programmers should treat instances that are
  4. * {@linkplain #equals(Object) equal} as interchangeable and should not
  5. * use instances for synchronization, or unpredictable behavior may
  6. * occur. For example, in a future release, synchronization may fail.
  7. *
  8. * <p>Implementation note: The implementations of the "bit twiddling"
  9. * methods (such as {@link #highestOneBit(int) highestOneBit} and
  10. * {@link #numberOfTrailingZeros(int) numberOfTrailingZeros}) are
  11. * based on material from Henry S. Warren, Jr.'s <i>Hacker's
  12. * Delight</i>, (Addison Wesley, 2002).
  13. *
  14. * @author Lee Boynton
  15. * @author Arthur van Hoff
  16. * @author Josh Bloch
  17. * @author Joseph D. Darcy
  18. * @since 1.0
  19. */
  20. @jdk.internal.ValueBased
  21. public final class Integer extends Number
  22. implements Comparable<Integer>, Constable, ConstantDesc {
  23. // ...
  24. /**
  25. * Constructs a newly allocated {@code Integer} object that
  26. * represents the specified {@code int} value.
  27. *
  28. * @param value the value to be represented by the
  29. * {@code Integer} object.
  30. *
  31. * @deprecated
  32. * It is rarely appropriate to use this constructor. The static factory
  33. * {@link #valueOf(int)} is generally a better choice, as it is
  34. * likely to yield significantly better space and time performance.
  35. */
  36. @Deprecated(since="9", forRemoval = true)
  37. public Integer(int value) {
  38. this.value = value;
  39. }
  40. // ...

添加@jdk.internal.ValueBased@Deprecated(since="9", forRemoval = true)的作用是什么呢?

  1. JDK设计者建议使用Integer a = 10或者Integer.valueOf()函数,而不是new Integer(),让其抛出告警?

在构造函数上都已经标记有@Deprecated(since="9", forRemoval = true)注解,这就意味着其构造函数在将来会被删除,不应该在程序中继续使用诸如new Integer(); 如果继续使用,编译期将会产生'Integer(int)' is deprecated and marked for removal 告警。

  1. 在并发环境下,Integer 对象根本无法通过 Synchronized 来保证线程安全,让其抛出告警?

由于JDK中对@jdk.internal.ValueBased注解加入了基于值的类的告警,所以继续在 Synchronized 同步块中使用值类型,将会在编译期和运行期产生警告,甚至是异常。

  1. public void inc(Integer count) {
  2. for (int i = 0; i < 10; i++) {
  3. new Thread(() -> {
  4. synchronized (count) { // 这里会产生编译告警
  5. count++;
  6. }
  7. }).start();
  8. }
  9. }
1.10.2.3、JEP 392:打包工具(正式版)

此特性最初是作为 Java 14 中的一个孵化器模块引入的,该工具允许打包自包含的 Java 应用程序。它支持原生打包格式,为最终用户提供自然的安装体验,这些格式包括 Windows 上的 msi 和 exe、macOS 上的 pkg 和 dmg,还有 Linux 上的 deb 和 rpm。它还允许在打包时指定启动时参数,并且可以从命令行直接调用,也可以通过 ToolProvider API 以编程方式调用。注意 jpackage 模块名称从 jdk.incubator.jpackage 更改为 jdk.jpackage。这将改善最终用户在安装应用程序时的体验,并简化了“应用商店”模型的部署。

1.10.2.4、JEP 396:默认强封装 JDK 内部元素

此特性会默认强封装 JDK 的所有内部元素,但关键内部 API(例如 sun.misc.Unsafe)除外。默认情况下,使用早期版本成功编译的访问 JDK 内部 API 的代码可能不再起作用。鼓励开发人员从使用内部元素迁移到使用标准 API 的方法上,以便他们及其用户都可以无缝升级到将来的 Java 版本。强封装由 JDK 9 的启动器选项–illegal-access 控制,到 JDK 15 默认改为 warning,从 JDK 16 开始默认为 deny。(目前)仍然可以使用单个命令行选项放宽对所有软件包的封装,将来只有使用–add-opens 打开特定的软件包才行。

1.10.3、JVM 优化

1.10.3.1、JEP 376:ZGC 并发线程处理

JEP 376 将 ZGC 线程栈处理从安全点转移到一个并发阶段,甚至在大堆上也允许在毫秒内暂停 GC 安全点。消除 ZGC 垃圾收集器中最后一个延迟源可以极大地提高应用程序的性能和效率。

1.10.3.2、JEP 387:弹性元空间

此特性可将未使用的 HotSpot 类元数据(即元空间,metaspace)内存更快速地返回到操作系统,从而减少元空间的占用空间。具有大量类加载和卸载活动的应用程序可能会占用大量未使用的空间。新方案将元空间内存按较小的块分配,它将未使用的元空间内存返回给操作系统来提高弹性,从而提高应用程序性能并降低内存占用。

1.10.4、新功能的预览和孵化

1.10.4.1、JEP 338:向量 API(孵化器)

如下内容来源于https://xie.infoq.cn/article/8304c894c4e38318d38ceb116,作者是九叔

AVX(Advanced Vector Extensions,高级向量扩展)实际上是 x86-64 处理器上的一套 SIMD(Single Instruction Multiple Data,单指令多数据流)指令集,相对于 SISD(Single instruction, Single dat,单指令流但数据流)而言,SIMD 非常适用于 CPU 密集型场景,因为向量计算允许在同一个 CPU 时钟周期内对多组数据批量进行数据运算,执行性能非常高效,甚至从某种程度上来看,向量运算似乎更像是一种并行任务,而非像标量计算那样,在同一个 CPU 时钟周期内仅允许执行一组数据运算,存在严重的执行效率低下问题。

随着 Java16 的正式来临,开发人员可以在程序中使用 Vector API 来实现各种复杂的向量计算,由 JIT 编译器 Server Compiler(C2)在运行期将其编译为对应的底层 AVX 指令执行。当然,在讲解如何使用 Vector API 之前,我们首先来看一个简单的标量计算程序。示例:

  1. void scalarComputation() {
  2. var a = new float[10000000];
  3. var b = new float[10000000];
  4. // 省略数组a和b的赋值操作
  5. var c = new float[10000000];
  6. for (int i = 0; i < a.length; i++) {
  7. c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
  8. }
  9. }

在上述程序示例中,循环体内每次只能执行一组浮点运算,总共需要执行约 1000 万次才能够获得最终的运算结果,可想而知,这样的执行效率必然低效。值得庆幸的是,从 Java6 的时代开始,Java 的设计者们就在 HotSpot 虚拟机中引入了一种被称之为 SuperWord 的自动向量优化算法,该算法缺省会将循环体内的标量计算自动优化为向量计算,以此来提升数据运算时的执行效率。当然,我们可以通过虚拟机参数-XX:-UseSuperWord来显式关闭这项优化(从实际测试结果来看,如果不开启自动向量优化,存在约 20%~22%之间的性能下降)。

在此大家需要注意,尽管 HotSpot 缺省支持自动向量优化,但局限性仍然非常明显,首先,JIT 编译器 Server Compiler(C2)仅仅只会对循环体内的代码块做向量优化,并且这样的优化也是极不可靠的;其次,对于一些复杂的向量运算,SuperWord 则显得无能为力。因此,在一些特定场景下(比如:机器学习,线性代数,密码学等),建议大家还是尽可能使用 Java16 为大家提供的 Vector API 来实现复杂的向量计算。示例:

  1. // 定义256bit的向量浮点运算
  2. static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256;
  3. void vectorComputation(float[] a, float[] b, float[] c) {
  4. var i = 0;
  5. var upperBound = SPECIES.loopBound(a.length);
  6. for (; i < upperBound; i += SPECIES.length()) {
  7. var va = FloatVector.fromArray(SPECIES, a, i);
  8. var vb = FloatVector.fromArray(SPECIES, b, i);
  9. var vc = va.mul(va).
  10. add(vb.mul(vb)).
  11. neg();
  12. vc.intoArray(c, i);
  13. }
  14. for (; i < a.length; i++) {
  15. c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
  16. }
  17. }

值得注意的是,Vector API 包含在 jdk.incubator.vector 模块中,程序中如果需要使用 Vector API 则需要在 module-info.java 文件中引入该模块。:

  1. module java16.test{
  2. requires jdk.incubator.vector;
  3. }
1.10.4.2、JEP 389:外部链接器 API(孵化器)

该孵化器 API 提供了静态类型、纯 Java 访问原生代码的特性,该 API 将大大简化绑定原生库的原本复杂且容易出错的过程。Java 1.1 就已通过 Java 原生接口(JNI)支持了原生方法调用,但并不好用。Java 开发人员应该能够为特定任务绑定特定的原生库。它还提供了外来函数支持,而无需任何中间的 JNI 粘合代码。

1.10.4.3、JEP 393:外部存储器访问 API(第三次孵化)

在 Java 14 和 Java 15 中作为孵化器 API 引入的这个 API 使 Java 程序能够安全有效地对各种外部存储器(例如本机存储器、持久性存储器、托管堆存储器等)进行操作。它提供了外部链接器 API 的基础。

如下内容来源于https://xie.infoq.cn/article/8304c894c4e38318d38ceb116,作者是九叔

在实际的开发过程中,绝大多数的开发人员基本都不会直接与堆外内存打交道,但这并不代表你从未接触过堆外内存,像大家经常使用的诸如:RocketMQ、MapDB 等中间件产品底层实现都是基于堆外存储的,换句话说,我们几乎每天都在间接与堆外内存打交道。那么究竟为什么需要使用到堆外内存呢?简单来说,主要是出于以下 3 个方面的考虑:

  • 减少 GC 次数和降低 Stop-the-world 时间;
  • 可以扩展和使用更大的内存空间;
  • 可以省去物理内存和堆内存之间的数据复制步骤。

在 Java14 之前,如果开发人员想要操作堆外内存,通常的做法就是使用 ByteBuffer 或者 Unsafe,甚至是 JNI 等方式,但无论使用哪一种方式,均无法同时有效解决安全性和高效性等 2 个问题,并且,堆外内存的释放也是一个令人头痛的问题。以 DirectByteBuffer 为例,该对象仅仅只是一个引用,其背后还关联着一大段堆外内存,由于 DirectByteBuffer 对象实例仍然是存储在堆空间内,只有当 DirectByteBuffer 对象被 GC 回收时,其背后的堆外内存才会被进一步释放。

在此大家需要注意,程序中通过 ByteBuffer.allocateDirect()方法来申请物理内存资源所耗费的成本远远高于直接在 on-heap 中的操作,而且实际开发过程中还需要考虑数据结构如何设计、序列化/反序列化如何支撑等诸多难题,所以与其使用语法层面的 API 倒不如直接使用 MapDB 等开源产品来得更实惠。

如今,在堆外内存领域,我们似乎又多了一个选择,从 Java14 开始,Java 的设计者们在语法层面为大家带来了崭新的 Memory Access API,极大程度上简化了开发难度,并得以有效的解决了安全性和高效性等 2 个核心问题。示例:

  1. // 获取内存访问var句柄
  2. var handle = MemoryHandles.varHandle(char.class,
  3. ByteOrder.nativeOrder());
  4. // 申请200字节的堆外内存
  5. try (MemorySegment segment = MemorySegment.allocateNative(200)) {
  6. for (int i = 0; i < 25; i++) {
  7. handle.set(segment, i << 2, (char) (i + 1 + 64));
  8. System.out.println(handle.get(segment, i << 2));
  9. }
  10. }

关于堆外内存段的释放,Memory Access API 提供有显式和隐式 2 种方式,开发人员除了可以在程序中通过 MemorySegment 的 close()方法来显式释放所申请的内存资源外,还可以注册 Cleaner 清理器来实现资源的隐式释放,后者会在 GC 确定目标内存段不再可访问时,释放与之关联的堆外内存资源。

1.10.4.4、JEP 397:密封类(第二预览)

封闭类可以是封闭类和或者封闭接口,用来增强 Java 编程语言,防止其他类或接口扩展或实现它们。这个特性由Java 15的预览版本晋升为正式版本。

  • 密封的类和接口解释和应用

因为我们引入了sealed classinterfaces,这些class或者interfaces只允许被指定的类或者interface进行扩展和实现。

使用修饰符sealed,您可以将一个类声明为密封类。密封的类使用reserved关键字permits列出可以直接扩展它的类。子类可以是最终的,非密封的或密封的。

之前我们的代码是这样的。

  1. public class Person { } //人
  2. class Teacher extends Person { }//教师
  3. class Worker extends Person { } //工人
  4. class Student extends Person{ } //学生

但是我们现在要限制 Person类 只能被这三个类继承,不能被其他类继承,需要这么做。

  1. // 添加sealed修饰符,permits后面跟上只能被继承的子类名称
  2. public sealed class Person permits Teacher, Worker, Student{ } //人
  3. // 子类可以被修饰为 final
  4. final class Teacher extends Person { }//教师
  5. // 子类可以被修饰为 non-sealed,此时 Worker类就成了普通类,谁都可以继承它
  6. non-sealed class Worker extends Person { } //工人
  7. // 任何类都可以继承Worker
  8. class AnyClass extends Worker{}
  9. //子类可以被修饰为 sealed,同上
  10. sealed class Student extends Person permits MiddleSchoolStudent,GraduateStudent{ } //学生
  11. final class MiddleSchoolStudent extends Student { } //中学生
  12. final class GraduateStudent extends Student { } //研究生

很强很实用的一个特性,可以限制类的层次结构。

1.10.5、提升 OpenJDK 开发人员的生产力

其余更改对 Java 开发人员(使用 Java 编写代码和运行应用程序的人员)不会直接可见,而只对 Java 开发人员(参与 OpenJDK 开发的人员)可见。

1.10.5.1、JEP 347:启用 C++14 语言特性(在 JDK 源代码中)

它允许在 JDK C++ 源代码中使用 C++14 语言特性,并提供在 HotSpot 代码中可以使用哪些特性的具体指导。在 JDK 15 中,JDK 中 C++ 代码使用的语言特性仅限于 C++98/03 语言标准。它要求更新各种平台编译器的最低可接受版本

1.10.5.2、JEP 357:从 Mercurial 迁移到 Git & JEP 369,迁移到 GitHub

这些 JEP 将 OpenJDK 社区的源代码存储库从 Mercurial(hg)迁移到 Git,并将它们托管在 GitHub 上以供 JDK 11 及更高版本使用,其中包括将 jcheck、webrev 和 defpath 工具等工具更新到 Git。Git 减小了元数据的大小(约 1/4),可节省本地磁盘空间并减少克隆时间。与 Mercurial 相比,现代工具链可以更好地与 Git 集成。

Open JDK Git 存储库现在位于 https://github.com/openjdk。

1.10.5.3、JEP 386:AlpineLinux 移植 & JEP 388:Windows/AArch64 移植

这些 JEP 的重点不是移植工作本身,而是将它们集成到 JDK 主线存储库中;JEP 386 将 JDK 移植到 Alpine Linux 和其他使用 musl 作为 x64 上主要 C 库的发行版上。此外,JEP 388 将 JDK 移植到 Windows AArch64(ARM64)。

 1.11、 Java 17 新特性概述

JDK 17 在 2021 年 9 月 14 号正式发布了!根据发布的规划,这次发布的 JDK 17 是一个长期维护的版本(LTS)。Java 17 提供了数千个性能稳定性安全性更新,以及 14 个 JEP(JDK 增强提案),进一步改进了 Java 语言和平台,以帮助开发人员提高工作效率。JDK 17 包括新的语言增强、库更新、对新 Apple (Mx CPU)计算机的支持、旧功能的删除和弃用,并努力确保今天编写的 Java 代码在未来的 JDK 版本中继续工作而不会发生变化。它还提供语言功能预览和孵化 API,以收集 Java 社区的反馈。@pdai

知识体系系统性梳理

1.11.1、语言特性增强

1.11.1.1、密封的类和接口(正式版)

封闭类可以是封闭类和或者封闭接口,用来增强 Java 编程语言,防止其他类或接口扩展或实现它们。这个特性由Java 15的预览版本晋升为正式版本。

  • 密封的类和接口解释和应用

因为我们引入了sealed classinterfaces,这些class或者interfaces只允许被指定的类或者interface进行扩展和实现。

使用修饰符sealed,您可以将一个类声明为密封类。密封的类使用reserved关键字permits列出可以直接扩展它的类。子类可以是最终的,非密封的或密封的。

之前我们的代码是这样的。

  1. public class Person { } //人
  2. class Teacher extends Person { }//教师
  3. class Worker extends Person { } //工人
  4. class Student extends Person{ } //学生

但是我们现在要限制 Person类 只能被这三个类继承,不能被其他类继承,需要这么做。

  1. // 添加sealed修饰符,permits后面跟上只能被继承的子类名称
  2. public sealed class Person permits Teacher, Worker, Student{ } //人
  3. // 子类可以被修饰为 final
  4. final class Teacher extends Person { }//教师
  5. // 子类可以被修饰为 non-sealed,此时 Worker类就成了普通类,谁都可以继承它
  6. non-sealed class Worker extends Person { } //工人
  7. // 任何类都可以继承Worker
  8. class AnyClass extends Worker{}
  9. //子类可以被修饰为 sealed,同上
  10. sealed class Student extends Person permits MiddleSchoolStudent,GraduateStudent{ } //学生
  11. final class MiddleSchoolStudent extends Student { } //中学生
  12. final class GraduateStudent extends Student { } //研究生

很强很实用的一个特性,可以限制类的层次结构。

  • 补充:它是由Amber项目孵化而来(会经历两轮以上预览版本)

什么是Amber项目?

Amber 项目的目标是探索和孵化更小的、以生产力为导向的 Java 语言功能,这些功能已被 OpenJDK JEP 流程接受为候选 JEP。本项目由 Compiler Group 赞助。 大多数 Amber 功能在成为 Java 平台的正式部分之前至少要经过两轮预览。对于给定的功能,每轮预览和最终标准化都有单独的 JEP。此页面仅链接到某个功能的最新 JEP。此类 JEP 可能会酌情链接到该功能的早期 JEP。

1.11.2、工具库的更新

1.11.2.1、JEP 306:恢复始终严格的浮点语义

Java 编程语言和 Java 虚拟机最初只有严格的浮点语义。从 Java 1.2 开始,默认情况下允许在这些严格语义中进行微小的变化,以适应当时硬件架构的限制。这些差异不再有帮助或必要,因此已被 JEP 306 删除。

1.11.2.2、JEP 356:增强的伪随机数生成器

为伪随机数生成器 (PRNG) 提供新的接口类型和实现。这一变化提高了不同 PRNG 的互操作性,并使得根据需求请求算法变得容易,而不是硬编码特定的实现。简单而言只需要理解如下三个问题: @pdai

1.11.2.3、JDK 17之前如何生成随机数
  1. Random 类

典型的使用如下,随机一个int值

  1. // random int
  2. new Random().nextInt();
  3. /**
  4. * description 获取指定位数的随机数
  5. *
  6. * @param length 1
  7. * @return java.lang.String
  8. */
  9. public static String getRandomString(int length) {
  10. String base = "abcdefghijklmnopqrstuvwxyz0123456789";
  11. Random random = new Random();
  12. StringBuilder sb = new StringBuilder();
  13. for (int i = 0; i < length; i++) {
  14. int number = random.nextInt(base.length());
  15. sb.append(base.charAt(number));
  16. }
  17. return sb.toString();
  18. }
  1. ThreadLocalRandom 类

提供线程间独立的随机序列。它只有一个实例,多个线程用到这个实例,也会在线程内部各自更新状态。它同时也是 Random 的子类,不过它几乎把所有 Random 的方法又实现了一遍。

  1. /**
  2. * nextInt(bound) returns 0 <= value < bound; repeated calls produce at
  3. * least two distinct results
  4. */
  5. public void testNextIntBounded() {
  6. // sample bound space across prime number increments
  7. for (int bound = 2; bound < MAX_INT_BOUND; bound += 524959) {
  8. int f = ThreadLocalRandom.current().nextInt(bound);
  9. assertTrue(0 <= f && f < bound);
  10. int i = 0;
  11. int j;
  12. while (i < NCALLS &&
  13. (j = ThreadLocalRandom.current().nextInt(bound)) == f) {
  14. assertTrue(0 <= j && j < bound);
  15. ++i;
  16. }
  17. assertTrue(i < NCALLS);
  18. }
  19. }
  1. SplittableRandom 类

非线程安全,但可以 fork 的随机序列实现,适用于拆分子任务的场景。

  1. /**
  2. * Repeated calls to nextLong produce at least two distinct results
  3. */
  4. public void testNextLong() {
  5. SplittableRandom sr = new SplittableRandom();
  6. long f = sr.nextLong();
  7. int i = 0;
  8. while (i < NCALLS && sr.nextLong() == f)
  9. ++i;
  10. assertTrue(i < NCALLS);
  11. }
1.11.2.4、为什么需要增强
  1. 上述几个类实现代码质量和接口抽象不佳
  2. 缺少常见的伪随机算法
  3. 自定义扩展随机数的算法只能自己去实现,缺少统一的接口
1.11.2.5、增强后是什么样的

代码的优化自不必说,我们就看下新增了哪些常见的伪随机算法

如何使用这个呢?可以使用RandomGenerator

RandomGenerator g = RandomGenerator.of("L64X128MixRandom");
1.11.2.6、JEP 382:新的macOS渲染管道

使用 Apple Metal API 为 macOS 实现 Java 2D 管道。新管道将减少 JDK 对已弃用的 Apple OpenGL API 的依赖。

目前默认情况下,这是禁用的,因此渲染仍然使用OpenGL API;要启用metal,应用程序应通过设置系统属性指定其使用:

-Dsun.java2d.metal=true

Metal或OpenGL的使用对应用程序是透明的,因为这是内部实现的区别,对Java API没有影响。Metal管道需要macOS 10.14.x或更高版本。在早期版本上设置它的尝试将被忽略。

1.11.3、新的平台支持

1.11.3.1、JEP 391:支持macOS AArch64

将 JDK 移植到 macOS/AArch64 平台。该端口将允许 Java 应用程序在新的基于 Arm 64 的 Apple Silicon 计算机上本地运行。

1.11.4、旧功能的删除和弃用

1.11.4.1、JEP 398:弃用 Applet API

所有网络浏览器供应商要么已取消对 Java 浏览器插件的支持,要么已宣布计划这样做。 Applet API 已于 2017 年 9 月在 Java 9 中弃用,但并未移除。

1.11.4.2、JEP 407:删除 RMI 激活

删除远程方法调用 (RMI) 激活机制,同时保留 RMI 的其余部分。

1.11.4.3、JEP 410:删除实验性 AOT 和 JIT 编译器

实验性的基于 Java 的提前 (AOT) 和即时 (JIT) 编译器是实验性功能,并未得到广泛采用。作为可选,它们已经从 JDK 16 中删除。这个 JEP 从 JDK 源代码中删除了这些组件。

1.11.4.4、JEP 411:弃用安全管理器以进行删除

安全管理器可以追溯到 Java 1.0。多年来,它一直不是保护客户端 Java 代码的主要方法,也很少用于保护服务器端代码。在未来的版本中将其删除将消除重大的维护负担,并使 Java 平台能够向前发展。

1.11.5、新功能的预览和孵化API

1.11.5.1、JEP 406:新增switch模式匹配(预览版)

允许针对多个模式测试表达式,每个模式都有特定的操作,以便可以简洁安全地表达复杂的面向数据的查询。

1.11.5.2、JEP 412:外部函数和内存api (第一轮孵化)

改进了 JDK 14 和 JDK 15 中引入的孵化 API,使 Java 程序能够与 Java 运行时之外的代码和数据进行互操作。通过有效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存,这些 API 使 Java 程序能够调用本地库和处理本地数据,而不会像 Java 本地接口 (JNI) 那样脆弱和复杂。这些 API 正在巴拿马项目中开发,旨在改善 Java 和非 Java 代码之间的交互。

1.11.5.3、JEP 414:Vector API(第二轮孵化)

如下内容来源于https://xie.infoq.cn/article/8304c894c4e38318d38ceb116,作者是九叔

AVX(Advanced Vector Extensions,高级向量扩展)实际上是 x86-64 处理器上的一套 SIMD(Single Instruction Multiple Data,单指令多数据流)指令集,相对于 SISD(Single instruction, Single dat,单指令流但数据流)而言,SIMD 非常适用于 CPU 密集型场景,因为向量计算允许在同一个 CPU 时钟周期内对多组数据批量进行数据运算,执行性能非常高效,甚至从某种程度上来看,向量运算似乎更像是一种并行任务,而非像标量计算那样,在同一个 CPU 时钟周期内仅允许执行一组数据运算,存在严重的执行效率低下问题。

随着 Java16 的正式来临,开发人员可以在程序中使用 Vector API 来实现各种复杂的向量计算,由 JIT 编译器 Server Compiler(C2)在运行期将其编译为对应的底层 AVX 指令执行。当然,在讲解如何使用 Vector API 之前,我们首先来看一个简单的标量计算程序。示例:

  1. void scalarComputation() {
  2. var a = new float[10000000];
  3. var b = new float[10000000];
  4. // 省略数组a和b的赋值操作
  5. var c = new float[10000000];
  6. for (int i = 0; i < a.length; i++) {
  7. c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
  8. }
  9. }

在上述程序示例中,循环体内每次只能执行一组浮点运算,总共需要执行约 1000 万次才能够获得最终的运算结果,可想而知,这样的执行效率必然低效。值得庆幸的是,从 Java6 的时代开始,Java 的设计者们就在 HotSpot 虚拟机中引入了一种被称之为 SuperWord 的自动向量优化算法,该算法缺省会将循环体内的标量计算自动优化为向量计算,以此来提升数据运算时的执行效率。当然,我们可以通过虚拟机参数-XX:-UseSuperWord来显式关闭这项优化(从实际测试结果来看,如果不开启自动向量优化,存在约 20%~22%之间的性能下降)。

在此大家需要注意,尽管 HotSpot 缺省支持自动向量优化,但局限性仍然非常明显,首先,JIT 编译器 Server Compiler(C2)仅仅只会对循环体内的代码块做向量优化,并且这样的优化也是极不可靠的;其次,对于一些复杂的向量运算,SuperWord 则显得无能为力。因此,在一些特定场景下(比如:机器学习,线性代数,密码学等),建议大家还是尽可能使用 Java16 为大家提供的 Vector API 来实现复杂的向量计算。示例:

  1. // 定义256bit的向量浮点运算
  2. static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256;
  3. void vectorComputation(float[] a, float[] b, float[] c) {
  4. var i = 0;
  5. var upperBound = SPECIES.loopBound(a.length);
  6. for (; i < upperBound; i += SPECIES.length()) {
  7. var va = FloatVector.fromArray(SPECIES, a, i);
  8. var vb = FloatVector.fromArray(SPECIES, b, i);
  9. var vc = va.mul(va).
  10. add(vb.mul(vb)).
  11. neg();
  12. vc.intoArray(c, i);
  13. }
  14. for (; i < a.length; i++) {
  15. c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
  16. }
  17. }

值得注意的是,Vector API 包含在 jdk.incubator.vector 模块中,程序中如果需要使用 Vector API 则需要在 module-info.java 文件中引入该模块。:

  1. module java16.test{
  2. requires jdk.incubator.vector;
  3. }
1.11.5.4、JEP 389:外部链接器 API(孵化器)

该孵化器 API 提供了静态类型、纯 Java 访问原生代码的特性,该 API 将大大简化绑定原生库的原本复杂且容易出错的过程。Java 1.1 就已通过 Java 原生接口(JNI)支持了原生方法调用,但并不好用。Java 开发人员应该能够为特定任务绑定特定的原生库。它还提供了外来函数支持,而无需任何中间的 JNI 粘合代码。

1.11.5.5、JEP 393:外部存储器访问 API(第三次孵化)

在 Java 14 和 Java 15 中作为孵化器 API 引入的这个 API 使 Java 程序能够安全有效地对各种外部存储器(例如本机存储器、持久性存储器、托管堆存储器等)进行操作。它提供了外部链接器 API 的基础。

如下内容来源于https://xie.infoq.cn/article/8304c894c4e38318d38ceb116,作者是九叔

在实际的开发过程中,绝大多数的开发人员基本都不会直接与堆外内存打交道,但这并不代表你从未接触过堆外内存,像大家经常使用的诸如:RocketMQ、MapDB 等中间件产品底层实现都是基于堆外存储的,换句话说,我们几乎每天都在间接与堆外内存打交道。那么究竟为什么需要使用到堆外内存呢?简单来说,主要是出于以下 3 个方面的考虑:

  • 减少 GC 次数和降低 Stop-the-world 时间;
  • 可以扩展和使用更大的内存空间;
  • 可以省去物理内存和堆内存之间的数据复制步骤。

在 Java14 之前,如果开发人员想要操作堆外内存,通常的做法就是使用 ByteBuffer 或者 Unsafe,甚至是 JNI 等方式,但无论使用哪一种方式,均无法同时有效解决安全性和高效性等 2 个问题,并且,堆外内存的释放也是一个令人头痛的问题。以 DirectByteBuffer 为例,该对象仅仅只是一个引用,其背后还关联着一大段堆外内存,由于 DirectByteBuffer 对象实例仍然是存储在堆空间内,只有当 DirectByteBuffer 对象被 GC 回收时,其背后的堆外内存才会被进一步释放。

在此大家需要注意,程序中通过 ByteBuffer.allocateDirect()方法来申请物理内存资源所耗费的成本远远高于直接在 on-heap 中的操作,而且实际开发过程中还需要考虑数据结构如何设计、序列化/反序列化如何支撑等诸多难题,所以与其使用语法层面的 API 倒不如直接使用 MapDB 等开源产品来得更实惠。

如今,在堆外内存领域,我们似乎又多了一个选择,从 Java14 开始,Java 的设计者们在语法层面为大家带来了崭新的 Memory Access API,极大程度上简化了开发难度,并得以有效的解决了安全性和高效性等 2 个核心问题。示例:

  1. // 获取内存访问var句柄
  2. var handle = MemoryHandles.varHandle(char.class,
  3. ByteOrder.nativeOrder());
  4. // 申请200字节的堆外内存
  5. try (MemorySegment segment = MemorySegment.allocateNative(200)) {
  6. for (int i = 0; i < 25; i++) {
  7. handle.set(segment, i << 2, (char) (i + 1 + 64));
  8. System.out.println(handle.get(segment, i << 2));
  9. }
  10. }

关于堆外内存段的释放,Memory Access API 提供有显式和隐式 2 种方式,开发人员除了可以在程序中通过 MemorySegment 的 close()方法来显式释放所申请的内存资源外,还可以注册 Cleaner 清理器来实现资源的隐式释放,后者会在 GC 确定目标内存段不再可访问时,释放与之关联的堆外内存资源。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/601747
推荐阅读
相关标签
  

闽ICP备14008679号