Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java16 的新特性

你好,我是看山。

本文收录在 《从小工到专家的 Java 进阶之旅》 系列专栏中。

从 2017 年开始,Java 版本更新策略从原来的每两年一个新版本,改为每六个月一个新版本,以快速验证新特性,推动 Java 的发展。从 《JVM Ecosystem Report 2021》 中可以看出,目前开发环境中有近半的环境使用 Java8,有近半的人转移到了 Java11,随着 Java17 的发布,相信比例会有所变化。

因此,准备出一个系列,配合示例讲解,阐述各个版本的新特性。

概述

Java16 是在 2021 年 3 月发布的一个短期版本,新增特性如下:

  • JEP 338:向量 API(孵化功能)
  • JEP 347:启用 C++ 14 语言特性
  • JEP 357:从 Mercurial 迁移到 Git
  • JEP 369:迁移到 GitGitHub
  • JEP 376:ZGC 的并发线程堆栈处理
  • JEP 380:支持 Unix 套接字
  • JEP 386:Alpine Linux 移植
  • JEP 387:弹性元空间
  • JEP 388:Windows/AArch64 移植
  • JEP 389:外部链接器 API(孵化功能)
  • JEP 390:对基于值的类设置“弃用移除”警告
  • JEP 392:打包工具
  • JEP 393:外部存储器访问 API(第三版孵化功能)
  • JEP 394:instanceof 模式匹配
  • JEP 395:Record 类型
  • JEP 396:默认强封装 JDK 内部元素
  • JEP 397:密封类(第二版预览功能)

接下来我们一起看看这些特性。

启用 C++ 14 语言特性(JEP 347)

在 JDK15 之前,JDK 中使用的 C++语言限制在 C++98/03 版本,没有办法使用更高级的特性,从 JDK16 开始,可以支持 C++14 的语言特性。

这一点更新对应用开发者可能关系不大,但是对于底层组件的开发者意义重大。Java 的版本更新迅速,C++的特性也是飞速更新,如果 JDK 还是限制在 C++98/03 版本,没有办法使用 C++11/14 中的高级特性,也是一种损失。

从 Mercurial 迁移到 Git(JEP 357、JEP 369)

这是两个提案,JEP 357 是将 OpenJDK 社区的源代码版本控制工具,从 Mercurial(hg)迁移到 Git,JEP 369 是将 OpenJDK 项目定向到 GitHub 中的仓库,我们可以看到从 OpenJDK 的 JIRA 工具中,代码提交和 Issue 预览的都是在 https://github.com/openjdk 中,有一部分是从 https://git.openjdk.java.net 重定向到 GitHub。

Mercurial(hg)是一个 Python 编写的跨平台的分布式版本控制软件,与 Git 是同一时代开始的工具,功能也是很强大,只是在发展过程中,有些方面稍弱于 Git,比如元数据的占用、与现代工具链的集成。所以 OpenJDK 转而投向了 Git 的怀抱。

ZGC 的并发线程堆栈处理(JEP 376)

ZGC 是在 Java11 引入的(参见 Java11 新特性),在 Java15 中正式特性(参见 Java15 新特性),可以用命令-XX:+UseZGC启用 ZGC。

ZGC 是一个并发的垃圾回收器,可以极大地提升 GC 的性能,支持任意堆大小而保持稳定的低延迟。在 128G 堆大小的测试中,ZGC 优势明显,找了一张网上的图片:

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

ZGC 的目标是实现垃圾回收与程序同时运行,将 STW 降低为 0,即不存在中断。目前在标记、重定位、参考处理、类卸载和跟处理阶段删除安全点处理。目前 ZGC 中仍然依靠安全点执行的包括部分的根处理和有时间限制的标记终止操作。这些根处理中有一项就是 Java 线程堆栈处理。随着线程数量增加,停顿时间增长。所以,我们需要实现并发的堆栈处理。目标包括:

  • ZGC 的安全点不再包含线程堆栈处理。
  • 使堆栈处理变得惰性、协作、并发和增量。
  • 从 ZGC 安全点中删除所有其他线程根处理。
  • 提供一种机制,其他 HotSpot 子系统(如 Loom 和 JFR)可以通过该机制惰性地处理堆栈。

支持 Unix 套接字(JEP 380)

对于本地进程间通信,Unix 套接字比 TCP/IP 更加安全高效。Unix 套接字一直是大多数 Unix 平台的一个特性,现在在 Windows 10 和 Windows Server 2019 也提供了支持。

所以在 Java16 中为java.nio.channels包的SocketChannelServerSocketChannel添加了 Unix(AF_UNIX)套接字支持。Unix 套接字用于同一主机上的进程间通信(IPC), 在很大程度上类似于 TCP/IP,区别在于套接字是通过文件系统路径名而不是 Internet 协议(IP)地址和端口号寻址的。对于本地进程间通信,Unix 套接字比 TCP/IP 环回连接更安全、更有效。

Alpine Linux 移植、Windows/AArch64 移植(JEP 386、JEP 388)

这些移植的价值不在于移植本身,而在于支持平台的多样性。Java 的口号是一次编写到处运行。既然要到处运行,就得支持各种平台。而且,针对不同的操作系统支持,还能给我们提供更多的选择。

Alpine Linux 是一个独立非商业的 Linux 发行版,体系非常小,一个容器需要不超过 8MB 的空间,磁盘最小仅需 130MB 存储。如果我们通过 jlink 生产 JDK,Docket 镜像可以减小到 38MB,这样在微服务部署过程中,可以减少很多磁盘占用,也能减少镜像传输、部署时间。

弹性元空间(JEP 387)

这是在 HotSpot 中的空间分配上的优化,将未使用的元空间(metaspace,也叫类的元空间)中的内容存返回给操作系统。

应用程序如果存在大量类加载和类卸载的动作时,会占用大量的元空间内存,这部分内存得不到释放,造成内存利用率低。现在的应用系统为了应对高并发的流量,动辄部署数十上百台实例,这将造成极大的资源浪费。

元空间的内存方式使用的是基于区域的内存管理方式(Region-based memory management),也就是每个分配的对象都被分配到一个区域中。这里的区域有不同的叫法:zone(区域)、arena(竞技场)、memory context(内存上下文)等。

当类被回收后,其元空间区域中的内存块会返回自由列表中,以便以后重新使用。当然,可能很长使用不会被重新使用。这样就会造成元空间中很多的内存碎片,这些都是被标记为占用的内存。如果没有碎片的内存空间,是可以返回给操作系统的。

在 JEP 387 特性中,提出使用基于伙伴的内存分配算法(Buddy memory allocation)改善元空间的内存使用,这种方式是一种在 Linux 内核中经过验证的成熟算法。这种算法是在很小的块(chunk)中分配内存,这会降低类加载器的开销。

同时,JEP 387 增加了内存延迟提交给内存区域的特性,这样就会减少那种申请了内存却不使用的情况。

最后,JEP 387 将元空间的内存区域设计为不同大小,可以满足不同大小需求的内存申请。

这些操作与 Java13 中对 ZGC 的增强特性很类似(参见 Java13 的新特性)。他山之石可以攻玉,我们不妨学习一下这些方式,对我们在以后的开发中提供思路。

对基于值的类设置“弃用移除”警告(JEP 390)

将基于值的类的公共构造函数设置启用移除警告。

比如Interger的构造函数上设置了@Deprecated(since="9", forRemoval = true)。如果某个类使用了Integer integer = new Integer(1);这种写法,通过javac命令编译时,会收到警告:[removal] Integer 中的 Integer(int) 已过时,且标记为待删除这种警告信息。

基于值的类在类定义上都会有@jdk.internal.ValueBased注解,比如java.lang.Integerjava.lang.Double等。这样的改动是为 Valhalla 项目做准备。

打包工具(JEP 392)

打包工具是在 Java14 中引入的孵化功能(参见 Java14 的新特性),可以打包成自包含的 Java 应用程序,比如 Windows 的 exe 和 msi、Mac 的 pkg 和 dmg、Linux 的 deb 和 rpm 等。

我们可以使用jlink创建一个最小可运行的模块包,然后使用jpackage将其构建成安装包:

jpackage --name myapp --input lib --main-jar main.jar

这里需要注意一点,因为已经成为正式功能,模块名从jdk.incubator.jpackage改为jdk.jpackage

instanceof 模式匹配(JEP 394)

instanceof 模式匹配首先在 Java14 中提供预览功能(参见 Java14 特性),可以提供instanceof更加简洁高效的实现,在 Java15 中进行了第二次预览,用于收集反馈,终于是多年的媳妇熬成婆,在 Java16 中成为正式功能。

我们再简单复习一下instanceof模式匹配的功能(详细使用可以移步 Java14 特性):

@Test
void test1() {
    final Object obj1 = "Hello, World!";
    int result = 0;
    if (obj1 instanceof String str) {
        result = str.length();
    } else if (obj1 instanceof Number num) {
        result = num.intValue();
    }

    Assertions.assertEquals(13, result);
}

Record 类型(JEP 395)

Record 类型用来增强 Java 语言特性,充当不可变数据载体。与 instanceof 模式匹配一样,Record 类型也是在 Java14 中提供预览功能(参见 Java14 新特性),在 Java15 中进行了第二次预览,用于收集反馈。

我们再简单复习一下 Record 类型的功能,比如,我们定义一个Person类:

public record Person(String name, String address) {
}

我们转换为之前的定义会是一坨下面这种代码:

public final class PersonBefore14 {
    private final String name;
    private final String address;

    public PersonBefore14(String name, String address) {
        this.name = name;
        this.address = address;
    }

    public String name() {
        return name;
    }

    public String address() {
        return address;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        PersonBefore14 that = (PersonBefore14) o;
        return Objects.equals(name, that.name) && Objects.equals(address, that.address);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, address);
    }

    @Override
    public String toString() {
        return "PersonBefore14{" +
                "name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

Record 类型特性有四个特性:

  1. 设计一个面向对象的结构,表达简单的信息聚合;
  2. 帮助开发人员专注于建模不可变数据,而不是可扩展的行为;
  3. 自动实现数据驱动的方法,比如equalsgettersetter等方法;
  4. 保留长期存在的 Java 规范,比如迁移兼容性。

我们不能将 Record 类型简单的理解为去除“样板化”代码的功能,它不是解决 JavaBean 命名约定的中很多模板化方法的冗余繁杂问题,它的目标不是类似 Lombok 等工具自动生成代码的功能,是从开发人员专注模型的角度出发的。

默认强封装 JDK 内部元素(JEP 396)

这个功能特性是为了改进 JDK 的安全性和可维护性,是 Jigsaw 项目的主要目标之一。所以在 Java16 中,默认强封装 JDK 的绝大部分内部 API,有些关键性的 API,比如sun.misc.Unsafe暂时可以放心使用。

我们可以使用启动参数--illegal-access控制内部 API 的封装程度:

  • --illegal-access=permit:JDK 8 中存在的每个包对未命名模块中的代码开放。也就是放心大胆地使用。Java9 中默认就是这个等级;
  • --illegal-access=warn:与许可相同,不同之处在于每次非法反射访问操作都会发出警告消息;
  • --illegal-access=debug:与 warn 相同,不同的是,每个非法反射访问操作都会发出警告消息和堆栈跟踪;
  • --illegal-access=deny:禁用所有非法访问操作,但由其他命令行选项(例如--add-opens)启用的操作除外。

预览

密封类-预览第二版(JEP 397)

密封类首次在 Java15 中预览(参见 Java15 新特性),在 Java16 中进行第二次预览,我们在复习一下功能:

public sealed interface JungleAnimal permits Monkey, Snake  {
}

public final class Monkey implements JungleAnimal {
}

public non-sealed class Snake implements JungleAnimal {
}

sealed关键字与permits关键字结合使用,以确定允许哪些类实现此接口。在我们的例子中,是MonkeySnake

  • sealed:必须使用permits关键字定义允许继承的子类;
  • final:最终类,不再有子类;
  • non-sealed:普通类,任何类都可以继承它。

孵化

向量 API(JEP 338)

这是为向量计算专门定义的 API,可以在运行时可靠地编译为支持的 CPU 架构上的最佳向量硬件指令,从而获得优于同等标量计算的性能,充分利用单指令多数据(SIMD)技术(大多数现代 CPU 上都可以使用的一种指令)。该 API 将使开发人员能够轻松地用 Java 编写可移植的高性能向量算法。

尽管 HotSpot 支持自动向量化,但是可转换的标量操作集有限且易受代码更改的影响。

final int[] a = {1, 2, 3, 4};
final int[] b = {5, 6, 7, 8};
final int[] c = new int[3];

IntVector vectorA = IntVector.fromArray(IntVector.SPECIES_128, a, 0);
IntVector vectorB = IntVector.fromArray(IntVector.SPECIES_128, b, 0);
IntVector vectorC = vectorA.mul(vectorB);
vectorC.intoArray(c, 0);

这个功能在 Java17 中进行了第二次孵化,基于使用安全的考虑,我们在短时间内用不上这个特性了。

外部链接器 API(JEP 389)

这个特性提供了静态类型、纯 Java 访问原生代码的 API,大大简化绑定原生库的原本复杂且容易出错的过程。从 Java1.1 开始,我们可以通过原生接口(JNI)调用原生方法,但是并不好用,现在提供了外部链接器 API,可以不再使用 JNI 粘合代码了。

和向量 API 一样,暂时用不上了,等啥时候转正了,咱们重点说说怎么玩。

外部存储器访问 API-孵化第三版(JEP 393)

外部存储器访问 API 在 Java14 开始孵化(参见 Java14 新特性),在 Java15 中孵化第二版(参见 Java15 新特性),在 Java16 中进行第三版孵化。

外部存储器访问 API 使 Java 程序能够安全有效地对各种外部存储器(例如本机存储器、持久性存储器、托管堆存储器等)进行操作。外部内存通常是说那些独立 JVM 之外的内存区域,可以不受 JVM 垃圾收集的影响,通常能够处理较大的内存。

这次带来的特性包括:

  • MemorySegmentMemoryAddress接口之间更加清晰的职责分离;
  • 增加了新接口MemoryAccess,提供了常见的静态内存访问器,以便在简单的情况下尽量减少对VarHandle 的需求;
  • 支持共享 segments,并提供向清理器注册 segments 的能力。

这些新的 API 虽然不会直接影响多数的应用类开发人员,但是他们可以在内存的第三方库中提供支持,包括分布式缓存、非结构化文档存储、大型字节缓冲区、内存映射文件等。

文末总结

本文介绍了 Java16 新增的特性,完整的特性清单可以从 https://openjdk.java.net/projects/jdk/16/ 查看。后续内容会发布在 从小工到专家的 Java 进阶之旅 系列专栏中。

青山不改,绿水长流,我们下次见。

推荐阅读


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。

个人主页:https://www.howardliu.cn
个人博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java16 的新特性
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java 每半年就会更新一次新特性,再不掌握就要落伍了:Java16 的新特性

👇🏻欢迎关注我的公众号「看山的小屋」,领取精选资料👇🏻

公众号:看山的小屋