精通-Java9-全-

精通 Java9(全)

原文:zh.annas-archive.org/md5/bf9c1320aeafd6aae0bbd7a381eb6d7a

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Java 9 及其新特性丰富了这种语言——构建健壮软件应用中最常用的语言之一。Java 9 特别强调模块化,由 Jigsaw 项目实现。本书是掌握 Java 平台变更的一站式指南。

本书概述并解释了 Java 9 中引入的新特性以及新 API 和增强的重要性。Java 9 的一些新特性具有划时代的意义,如果你是一位经验丰富的程序员,通过实施这些新特性,你将能够使你的企业应用更加精简。你将获得关于如何应用新获得的 Java 9 知识的实用指导,以及关于 Java 平台未来发展的更多信息。本书将提高你的生产力,使你的应用更快。通过学习 Java 的最佳实践,你将成为你组织中 Java 9 的首选人员。

在本书结束时,你不仅将了解 Java 9 的重要概念,还将对使用这种伟大语言的编程重要方面有深入的理解。

本书涵盖内容

第一章,Java 9 景观,探讨了在 Java 9 中引入的最显著特性,包括 Jigsaw 项目、Java Shell、G1 垃圾收集和响应式编程。本章为这些主题提供了介绍,为后续章节的深入探讨奠定了基础。

第二章,探索 Java 9,涵盖了 Java 平台的一些变更,包括堆空间效率、内存分配、编译过程改进、类型测试、注解、自动运行时编译器测试和改进的垃圾收集。

第三章,Java 9 语言增强,专注于对 Java 语言的修改。这些修改影响了变量处理器、弃用警告、对 Java 7 中实现的 Project Coin 变更的改进以及导入语句处理。

第四章,使用 Java 9 构建模块化应用,检查了由 Project Jigsaw 指定的 Java 模块结构以及 Project Jigsaw 如何作为 Java 平台的一部分得到实现。本章还回顾了与新的模块化系统相关的 Java 平台的关键内部变更。

第五章,迁移应用至 Java 9,探讨了如何将 Java 8 应用迁移到 Java 9 平台。涵盖了手动和半自动迁移过程。

第六章,Java Shell 实验,涵盖了 Java 9 中的新命令行读取-评估-打印循环工具 JShell。内容包括关于该工具、读取-评估-打印循环概念以及与 JShell 一起使用的命令和命令行选项的信息。

第七章,利用新的默认 G1 垃圾收集器,深入探讨了垃圾收集以及它在 Java 9 中的处理方式。

第八章,使用 JMH 进行微基准测试应用,探讨了如何使用 Java Microbenchmark Harness(JMH)编写性能测试,JMH 是一个用于为 Java 虚拟机(JVM)编写基准测试的 Java 工具库。Maven 与 JMH 一起使用,以帮助说明使用新的 Java 9 平台进行微基准测试的强大功能。

第九章,利用 ProcessHandle API,回顾了新的类 API,这些 API 能够管理操作系统进程。

第十章,细粒度堆栈跟踪,涵盖了允许有效堆栈跟踪的新 API。本章包括如何访问堆栈跟踪信息的详细信息。

第十一章,新工具和工具增强,涵盖了 16 个被纳入 Java 9 平台的 Java 增强提案(JEPs)。这些 JEPs 覆盖了广泛的工具和 API 更新,以使使用 Java 进行开发更加容易,并为我们的 Java 应用程序提供更大的优化可能性。

第十二章,并发增强,涵盖了 Java 9 平台引入的并发增强。主要关注对响应式编程的支持,这是由 Flow 类 API 提供的并发增强。Java 9 中引入的其他并发增强也进行了介绍。

第十三章,安全增强,涵盖了针对 JDK 进行的一些涉及安全性的小改动。Java 9 平台引入的安全增强为开发者提供了更大的能力,以编写和维护比以前更安全的应用程序。

第十四章,命令行标志,探讨了 Java 9 中的命令行标志更改。本章涵盖的概念包括统一的 JVM 日志记录、编译器控制、诊断命令、堆分析代理、JHAT、命令行标志参数验证以及为旧平台版本编译。

第十五章,Java 9 最佳实践,专注于使用 Java 9 平台提供的实用程序,包括 UTF-8 属性文件、Unicode 7.0.0、Linux/AArch64 端口、多分辨率图像和常见区域数据存储库。

第十六章,未来方向,概述了 Java 平台未来的发展,超出了 Java 9 的范围。这包括对 Java 10 的计划以及我们预计未来将看到的变化的具体探讨。

您需要为此书准备的内容

要使用此文本,您至少需要具备 Java 编程语言的基本知识。

您还需要以下软件组件:

本书面向的对象

此书面向企业开发人员和现有的 Java 开发人员。Java 基础知识是必要的。

规范

在此书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL 和用户输入如下所示:“在C:\chapter8-benchmark\src\main\java\com\packt子目录结构下是MyBenchmark.java文件。”

代码块设置如下:

    public synchronized void protectedMethod()
    {
      . . . 
    }

新术语重要词汇以粗体显示。

警告或重要提示如下所示。

技巧和窍门如下所示。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要向我们发送一般反馈,请简单地发送电子邮件至feedback@packtpub.com,并在邮件主题中提及书的标题。

如果您在某个主题上具有专业知识,并且您有兴趣撰写或为此书做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您充分利用您的购买。

下载示例代码

您可以从www.packtpub.com的账户下载此书的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的 SUPPORT 标签上。

  3. 点击代码下载与勘误。

  4. 在搜索框中输入书的名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买此书的来源。

  7. 点击代码下载。

文件下载完成后,请确保使用最新版本的以下软件解压或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Mastering-Java-9。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

错误清单

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息来报告它们。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站或添加到该标题的错误清单部分。

要查看之前提交的错误清单,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在错误清单部分。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过copyright@packtpub.com与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。

问题

如果您对本书的任何方面有问题,您可以通过questions@packtpub.com与我们联系,我们将尽力解决问题。

第一章:Java 9 的景象

Java 自从第一个版本发布以来,已经是一个成熟的成年人了。拥有令人惊叹的开发者社区,并在多个行业中得到广泛应用,该平台继续发展和与世界其他地区保持一致,无论是在性能、安全性还是可扩展性方面。我们将从探索 Java 9 引入的最显著特性开始,探讨它们背后的最大推动力,以及我们可以在平台后续发展中期待什么,以及一些没有包含在本版本中的内容。

在本章中,我们将涵盖以下主题:

  • Java 9 从 20,000 英尺的高度看

  • 打破单体架构

  • 在 Java Shell 中玩耍

  • 掌控外部进程

  • 使用 G1 提升性能

  • 使用 JMH 测量性能

  • 准备迎接 HTTP 2.0

  • 包含响应式编程

  • 扩展愿望清单

Java 9 从 20,000 英尺的高度看

你可能会问自己——Java 9 不就是一个包含了一些没有进入 Java 8 的特性的维护版本吗?Java 9 中有很多新特性,使其成为一个独特的版本。

毫无疑问,Java 平台(作为 Jigsaw 项目的一部分开发)的模块化是使其在 Java 9 中成功的关键部分。最初计划在 Java 8 中实施,但被推迟,Jigsaw 项目也是 Java 9 最终发布进一步推迟的主要原因之一。Jigsaw 还引入了 Java 平台的一些显著变化,这也是 Java 9 被认为是重大版本的原因之一。我们将在后续章节中详细探讨这些特性。

JCPJava 社区进程)提供了将一组特性提案(也称为Java 增强提案JEPs)转化为正式规范的手段,这些规范为通过新功能扩展平台提供了基础。在这方面,Java 9 并无不同。除了与 Jigsaw 相关的 Java 增强提案外,还有许多其他增强功能被纳入 Java 9。在本书中,我们将根据相应的增强提案,以逻辑组的形式讨论各种特性,包括以下内容:

  • Java Shell(也称为JShell)——Java 平台的交互式 Shell

  • 新的 API,用于以可移植的方式与操作系统进程一起工作

  • 在 Java 7 中引入的垃圾收集器G1)在 Java 9 中被设置为默认的垃圾收集器

  • 添加Java 微基准工具JMH),该工具可以用于对 Java 应用程序进行性能基准测试,作为 Java 发行版的一部分

  • 通过新的客户端 API 支持 HTTP 2.0 和 WebSocket 标准

  • 并发增强,其中定义了Flow类,该类描述了 Java 平台中响应式流规范的接口

一些最初被接受用于发布 9 的提案没有成功实现,并推迟到了以后的版本,以及其他开发者可能期待的未来有趣事物。

如果您渴望在尝试浏览其他章节和尝试新引入的示例和概念之前动手实践,可以从 www.oracle.com/technetwork/java/javase/downloads/index.html 下载适用于您系统的 JDK 9 分发版。

打破单体

几年来,Java 平台的实用性持续发展和增加,使其成为了一个庞大的单体。为了使平台更适合嵌入式和移动设备,有必要发布简化版,如 Java CDC 和 Java ME。然而,这些版本在满足 JDK 提供的功能性方面变化多端的应用需求时,证明还不够灵活。在这方面,模块化系统的需求变得迫切,不仅是为了解决 Java 工具的模块化(总体而言,超过 5000 个 Java 类和 1500 个 C++ 源文件,Hotspot 运行时拥有超过 25,0000 行代码),而且还为了提供一个机制,让开发者可以使用与 JDK 中相同的模块系统来创建和管理模块化应用程序。Java 8 提供了一种中间机制,允许应用程序仅使用整个 JDK 提供的 API 子集,这种机制被命名为紧凑配置文件。实际上,紧凑配置文件还为后续工作奠定了基础,这些工作是为了打破 JDK 各个不同组件之间的依赖关系,从而实现 Java 中的模块化。

模块化系统本身是在名为 Jigsaw 项目的名称下开发的,基于此,形成了几个 Java 增强提案和一个目标 JSR(376)。为了满足 Jigsaw 项目的需求,已经做了很多工作——有概念实现的经验,提出的特性比成功进入 Java 9 的特性要多。除此之外,还对 JDK 代码库进行了完全重构,并对 JDK 可分发镜像进行了完全重组。

在社区中,关于是否应该采用现有的成熟 Java 模块系统(如 OSGi)作为 JDK 的一部分,而不是提供一个全新的模块系统,存在相当大的争议。然而,OSGi 针对运行时行为,如模块依赖的解析、安装、卸载、模块(在 OSGi 中也称为捆绑包)的启动和停止、自定义模块类加载器等。然而,Project Jigsaw 针对的是编译时模块系统,其中依赖关系的解析发生在应用程序编译时。此外,将模块作为 JDK 的一部分进行安装和卸载,消除了在编译时显式将其作为依赖项包含的需要。此外,通过现有的类加载器层次结构(引导、扩展和系统类加载器),可以实现模块类的加载,尽管,使用与 OSGi 模块类加载器相当的自定义模块类加载器是可能的。然而,后者已被放弃;当我们讨论 Java 模块系统细节时,我们将更详细地讨论 Java 模块类加载。

Java 模块系统带来的额外好处包括增强的安全性和性能。通过将 JDK 和应用程序模块化成 Jigsaw 模块,我们能够在组件及其对应的领域之间创建明确的边界。这种关注点的分离与平台的安全架构相一致,并且是提高资源利用率的推动力。我们专门用两章详细介绍了所有上述要点,以及采用 Java 9 的主题,这也需要对将现有项目迁移到 Java 9 的可能方法有一定的了解。

在 Java Shell 中玩耍

很长一段时间以来,Java 编程语言没有提供标准 shell 以供实验新的语言特性或库,或者用于快速原型设计。如果您想这样做,可以编写一个带有 main 方法的测试应用程序,使用 javac 编译它,然后运行。这可以在命令行或使用 Java IDE 中完成;然而,在这两种情况下,这都不如拥有一个交互式 shell 来得方便。

在 JDK 9 中启动交互式 shell 与运行以下命令一样简单(假设 JDK 9 安装中的 bin 目录位于当前路径):

jshell

你可能会觉得有些困惑,因为在 Java 平台上,交互式外壳之前没有被引入,就像许多编程语言(如 Python、Ruby 以及其他一些语言)在它们的早期版本中已经包含了交互式外壳一样;然而,这仍然没有成为早期 Java 版本的优先功能列表,直到现在,它已经出现并准备好使用。Java 外壳利用了 JShell API,该 API 提供了启用自动完成或评估表达式和代码片段等功能。有一整章专门讨论 Java 外壳的细节,以便开发者能够最大限度地利用它。

在 JDK 9 之前,如果你想要创建一个 Java 进程并处理进程的输入/输出,你必须使用 Runtime.getRuntime.exec() 方法,它允许我们在单独的 OS 进程中执行一个命令,并获取一个 java.lang.Process 实例,通过它提供某些操作来管理外部进程,或者使用新的 java.lang.ProcessBuilder 类,它在与外部进程交互方面提供了一些增强,并创建了一个 java.lang.Process 实例来表示外部进程。这两种机制都不灵活,也不便携,因为外部进程执行的命令集高度依赖于操作系统(需要付出额外的努力才能使特定的进程操作在多个操作系统之间便携)。有一章专门介绍新的进程 API,为开发者提供创建和管理外部进程的更简单方式的知识。

控制外部进程

使用 G1 提升性能

G1 垃圾收集器在 JDK 7 中已经被引入,现在在 JDK 9 中默认启用。它针对的是具有多个处理核心和大量可用内存的系统。与之前的垃圾收集器类型相比,G1 的优势是什么?它是如何实现这些改进的?是否需要手动调整它,以及在什么场景下?这些问题以及更多关于 G1 的问题将在单独的章节中讨论。

使用 JMH 测量性能

在许多情况下,Java 应用程序可能会遭受性能下降。加剧这个问题的是缺乏性能测试,这些测试至少可以提供一组保证,即满足性能要求,而且某些功能的性能不会随时间下降。测量 Java 应用程序的性能并不简单,特别是由于存在许多可能影响性能统计信息的编译器和运行时优化。因此,必须使用额外的措施,如预热阶段和其他技巧,以提供更准确的性能测量。Java Microbenchmark Harness 是一个框架,它结合了多种技术,并提供了方便的 API,可用于此目的。它不是一个新工具,但包含在 Java 9 的发行版中。如果您还没有将 JMH 添加到您的工具箱中,请阅读关于在 Java 9 应用程序开发中使用 JMH 的详细章节。

开始使用 HTTP 2.0

HTTP 2.0 是 HTTP 1.1 协议的继任者,这个新版本的协议解决了前一个版本的一些局限性和缺点。HTTP 2.0 在多个方面提高了性能,并提供了如请求/响应多路复用、服务器推送响应、流量控制和请求优先级等功能。

Java 提供了java.net.HttpURLConnection实用工具,可用于建立非安全的 HTTP 1.1 连接。然而,该 API 被认为难以维护,并且随着对 HTTP 2.0 的支持而进一步扩展,因此引入了一个全新的客户端 API,用于通过 HTTP 2.0 或 WebSocket 协议建立连接。新的 HTTP 2.0 客户端及其提供的能力将在专门的章节中介绍。

包含响应式编程

响应式编程是一种用于描述系统变化传播特定模式的范式。响应性并非 Java 本身所固有的,但可以使用 RxJava 或项目 Reactor(Spring 框架的一部分)等第三方库建立响应式数据流。JDK 9 也通过提供java.util.concurrent.Flow类来满足开发基于响应式流理念的高度响应性应用程序的 API 需求。Flow类以及 JDK 9 中引入的其他相关更改将在单独的章节中介绍。

扩展愿望清单

除了 JDK 9 中的所有新功能外,平台未来版本预计还将引入一系列全新的功能。其中以下是一些:

  • 原始类型上的泛型:这是 JDK 10 计划中的功能之一,作为 Valhalla 项目的一部分。其他语言增强功能,如值处理,已经包含在 Java 9 中,并将在此书的后续章节中介绍。

  • 具现泛型:这是 Valhalla 项目另一个特色部分,旨在提供在运行时保留泛型类型的能力。相关目标如下:

    • 外部功能接口旨在引入一个新的 API 来调用和管理本地函数。该 API 解决了 JNI 的一些缺点,特别是为应用程序开发者使用时的缺乏简便性。外部功能接口是作为 JDK 生态系统中的 Panama 项目的一部分开发的。

    • 新的货币和货币 API(在 JSR 354 下开发)最初计划在 Java 9 中推出,但被推迟。

    • 新的轻量级 JSON API(在 JSR 353 下开发)也计划在 Java 9 中推出,但推迟到 Java 10。

这些只是 JDK 后续版本中可能期待的一些新功能。Penrose 项目旨在弥合 Java 模块系统与 OSGi 模块系统之间的差距,并为两个系统之间的互操作性提供不同的方法。

Graal VM 是另一个有趣的研究项目,是 Java 平台后续版本的一个潜在候选者。它旨在将 Java 的运行时性能提升到动态语言如 JavaScript 或 Ruby 的水平。

一章专门讨论 JDK 的未来,详细讨论了所有这些观点。

摘要

在这一简短的介绍性章节中,我们揭示了 JDK 9 提供的功能小宇宙。在这个平台版本中引入的模块系统无疑是 Java 应用程序开发的一个基石。我们还发现,JDK 9 中引入了许多其他重要功能和变更,值得特别关注,将在后续章节中详细讨论。

在下一章中,我们将探讨 Java 平台 26 项内部变更。

第二章:发现 Java 9

Java 9 代表了一个主要版本,包括对 Java 平台的大量内部更改。这些内部更改共同为 Java 开发者提供了一组巨大的新可能性,其中一些来自开发者的请求,另一些来自 Oracle 启发的增强。在本章中,我们将回顾 26 个最重要的更改。每个更改都与一个JDK 增强提案JEP)相关。JEPs 被索引并托管在openjdk.java.net/jeps/0。您可以访问此网站以获取有关每个 JEP 的更多信息。

JEP 项目是 Oracle 对开源、开源创新和开源标准的支持的一部分。虽然可以找到其他开源 Java 项目,但 OpenJDK 是 Oracle 唯一支持的项目。

在本章中,我们将介绍 Java 平台的变化。这些变化具有几个令人印象深刻的含义,包括:

  • 堆空间效率

  • 内存分配

  • 编译过程改进

  • 类型测试

  • 注解

  • 自动化的运行时编译器测试

  • 改进的垃圾收集

改进的竞争锁 [JEP 143]

JVM 使用堆空间为类和对象分配内存。每当创建一个对象时,JVM 都会在堆上分配内存。这有助于促进 Java 的垃圾收集,释放不再有引用的对象之前使用的内存。Java 栈内存略有不同,通常比堆内存小得多。

JVM 在管理多个线程共享的数据区域方面做得很好。它将一个监控器与每个对象和类关联;这些监控器有由单个线程在任何时候控制的锁。这些由 JVM 控制的锁本质上是在给控制线程提供对象的监控器。

那么,什么是竞争锁?当一个线程正在等待当前已锁定对象的队列中时,它被说是在竞争该锁。以下图表展示了这种竞争的高级视图:

图片

如前图所示,任何等待的线程在对象释放之前都不能使用已锁定的对象。

改进目标

JEP 143 的一般目标是提高 JVM 管理锁定 Java 对象监控的总体性能。对竞争锁的改进全部在 JVM 内部进行,不需要任何开发者采取行动即可从中受益。总体改进目标与更快操作相关。这包括:

  • 更快的监控进入

  • 更快的监控退出

  • 更快的通知

通知是当对象锁定状态改变时调用的notify()notifyAll()操作。测试这种改进不是一件容易的事情。任何级别的更高效率都是受欢迎的,因此这种改进是我们即使没有容易观察到的测试也可以感激的。

分段代码缓存 [JEP 197]

分段代码缓存 JEP(197)的升级已完成,并导致执行时间更快、更高效。这一变化的核心是将代码缓存分为三个不同的段——非方法、分析和非分析代码。

代码缓存是 Java 虚拟机存储生成的本地代码的区域。

所述的上述代码缓存段将包含特定类型的编译代码。正如您在以下图中可以看到的,代码堆区域根据编译代码的类型进行了分段:

图片

内存分配

包含非方法代码的代码堆是用于 JVM 内部代码的,由一个 3MB 的固定内存块组成。其余的代码缓存内存平均分配给分析代码和非分析代码段。您可以通过命令行命令来控制这一点。

以下命令可以用来定义非方法编译代码的代码堆大小:

-XX:NonMethodCodeCodeHeapSize

以下命令可以用来定义分析编译方法的代码堆大小:

-XX:ProfiledCodeHeapSize

以下命令可以用来定义非分析编译方法的代码堆大小:

-XX:NonProfiledCodeHeapSize

这个 Java 9 特性确实可以改善 Java 应用程序的效率。它还影响了使用代码缓存的其他进程。

智能 Java 编译,第二阶段 [JEP 199]

JDK 增强提案 199 旨在改进代码编译过程。所有 Java 开发者都应该熟悉用于将源代码编译成字节码的javac工具,该工具由 JVM 用于运行 Java 程序。智能 Java 编译,也称为 Smart Javac 和sjavac,在 javac 进程周围添加了一个智能包装器。也许 sjavac 添加的核心改进是只重新编译必要的代码。在这个上下文中,必要的代码是指自上次编译周期以来已更改的代码。

如果开发者只在小项目上工作,这个增强可能不会让他们感到兴奋。然而,考虑到在中等和大型项目中您必须不断重新编译代码时效率的巨大提升,开发者可以节省的时间就足以让他们接受 JEP 199。

这将如何改变您编译代码的方式?可能不会,至少目前不会。Javac 将仍然是默认的编译器。虽然 sjavac 在增量构建方面提供了效率,但 Oracle 认为它没有足够的稳定性成为标准编译工作流程的一部分。

您可以在此处阅读有关智能 javac 包装器工具的更多信息:cr.openjdk.java.net/~briangoetz/JDK-8030245/webrev/src/share/classes/com/sun/tools/sjavac/Main.java-.html

解决 Lint 和 Doclint 警告 [JEP 212]

如果你对 Java 中的 Lint 或 Doclint 不熟悉,请不要担心。正如标题所示,它们是向 javac 报告警告的来源。让我们逐一看看:

  • Lint 分析 javac 的字节码和源代码。Lint 的目标是识别被分析代码中的安全漏洞。Lint 还可以提供关于可扩展性和线程锁定问题的见解。Lint 还有更多功能,其总体目的是节省开发者的时间。

你可以在这里了解更多关于 Lint 的信息:en.wikipedia.org/wiki/Lint_(software)

  • Doclint 与 Lint 类似,但特定于 javadoc。Lint 和 Doclint 都在编译过程中报告错误和警告。JEP 212 的重点是解决这些警告。当使用核心库时,不应该有任何警告。这种思维方式导致了 JEP 212,它已在 Java 9 中得到解决并实现。

可以在 bugs.openjdk.java.net JDK Bug 系统中查看 Lint 和 Doclint 警告的完整列表。

javac 的分层归因 [JEP 215]

JEP 215 代表了对 javac 类型检查方案的优化工作。让我们首先回顾一下 Java 8 中类型检查是如何工作的;然后我们将探讨 Java 9 中的变化。

在 Java 8 中,多态表达式的类型检查由一个推测性归因工具处理。

推测性归因是类型检查的一种方法,它是 javac 编译过程的一部分。它有显著的计算开销。

使用推测性归因方法进行类型检查是准确的,但效率不高。这些检查包括参数位置,在递归、多态、嵌套循环和 Lambda 表达式中测试时,速度会呈指数级下降。因此,JEP 215 的目标是将类型检查方案改为创建更快的结果。使用推测性归因的结果本身并没有不准确,只是生成速度不够快。

与 Java 9 一起发布的新方法使用分层归因工具。此工具为所有方法调用中的参数表达式实现分层类型检查方法。还允许方法重写。为了使此新方案工作,为以下列出的每种方法参数类型创建了新的结构类型:

  • Lambda 表达式

  • 多态表达式

  • 常规方法调用

  • 方法引用

  • 钻石实例创建表达式

JEP 215 对 javac 的更改比本节中突出显示的更复杂。除了更高效的 javac 和节省时间之外,对开发者的直接影响并不立即显现。

注解管道 2.0 [JEP 217]

Java 注解指的是位于你的 Java 源代码文件中的特殊类型元数据。它们不会被 javac 移除,这样它们就可以在运行时对 JVM 可用。

注释看起来类似于 JavaDocs 引用,因为它们以 @ 符号开头。有三种类型的注释。让我们逐一检查:

  • 最基本的注释形式是 标记 注释。这些是独立的注释,唯一的组件是动画的名称。以下是一个示例:
        @thisIsAMarkerAnnotation
        public double computeSometing(double x, double y) 
        {
          // do something and return a double
        }
  • 第二种注释类型是包含 单个值 或数据片段的注释。正如您在以下代码中所见,以 @ 符号开头的注释后面跟着包含数据的括号:
        @thisIsAMarkerAnnotation (data="compute x and y 
         coordinates")
        public double computeSometing(double x, double y) 
        {
          // do something and return a double
        }

编码单值注释类型的一种替代方法是省略 data= 组件,如下面的代码所示:

        @thisIsAMarkerAnnotation ("compute x and y coordinates")
        public double computeSometing(double x, double y) 
        {
          // do something and return a double
        }
  • 第三种注释类型是当存在 多个数据组件 时。在这种类型的注释中,不能省略 data= 组件。以下是一个示例:
        @thisIsAMarkerAnnotation (data="compute x and y 
         coordinates", purpose="determine intersecting point")
         public double computeSometing(double x, double y) 
         {
           // do something and return a double
         }

那么,Java 9 中有哪些变化?要回答这个问题,我们需要回顾一下 Java 8 中引入的一些变化,这些变化影响了 Java 注释:

  • Lambda 表达式

  • 重复的注释

  • Java 类型注释

这些与 Java 8 相关的更改影响了 Java 注释,但并没有导致 javac 处理它们的方式发生变化。有一些硬编码的解决方案允许 javac 处理新的注释,但它们效率不高。此外,这种编码(硬编码解决方案)难以维护。

因此,JEP 217 专注于重构 javac 注释管道。这次重构完全是 javac 内部的,因此它不应该对开发者明显。

新的版本字符串方案 [JEP 223]

在 Java 9 之前,发布号没有遵循行业标准版本控制——语义版本控制。例如,在撰写本文时,最后四个 JDK 发布版本是:

  • JDK 8 更新 131

  • JDK 8 更新 121

  • JDK 8 更新 112

语义版本控制 使用主版本、次版本、补丁版本(0.0.0)的方案:

主要 相当于引入了不向后兼容的新 API 变更。

次要 指的是添加的功能是向后兼容的。

补丁 指的是向后兼容的错误修复或小更改。

Oracle 已经接受了 Java 9 及以后的语义版本控制。对于 Java,Java 版本号的前三个元素将使用 主要-次要-安全 方案:

  • 主要:包含一组重大新功能的重大版本

  • 次要:向后兼容的修订和错误修复

  • 安全:被认为对提高安全性至关重要的修复

JEP 223 的这一描述可能会让版本控制方案看起来很基础。相反,已经制定了一套非常详细的规则和实践来管理未来的版本号。为了展示其复杂性,请看以下示例:

    1.9.0._32.b19

自动生成运行时编译器测试 [JEP 233]

Java 可以说是最常用的编程语言,并且运行在日益多样化的平台上。这加剧了在高效方式下运行针对编译器的测试的问题。JEP 233 的目的是创建一个可以自动化运行时编译器测试的工具。

创建的工具首先生成一组随机的 Java 源代码和/或字节码。生成的代码将具有三个关键特征:

  • 语法正确

  • 语义正确

  • 使用允许重用相同随机生成代码的随机种子

随机生成的源代码将被保存在以下目录中:

    hotspot/test/testlibrary/jit-tester

这些测试用例将被存储以供以后重用。可以从j-treg目录或从工具的 makefile 中运行。重新运行保存的测试的好处之一是测试系统的稳定性。

测试由 Javac 生成的类文件属性[JEP 235]

缺乏或不足的创建类文件属性测试的能力是 JEP 235 背后的动力。目标是确保 javac 完全且正确地创建类文件的属性。这表明即使某些属性没有被类文件使用,所有类文件都应该生成一个完整的属性集。还需要有一种方法来测试类文件是否正确创建,特别是关于文件的属性。

在 Java 9 之前,没有测试类文件属性的方法。运行类并测试代码以获得预期或期望的结果是测试 javac 生成的类文件最常用的方法。这种技术不足以测试以验证文件的属性。

类文件属性有三个类别——JVM 使用的属性、可选属性和 JVM 未使用的属性。

JVM 使用的属性包括:

  • BootstrapMethods

  • Code

  • ConstantValue

  • Exceptions

  • StackMapTable

可选属性包括:

  • Deprecated

  • LineNumberTable

  • LocalVariableTable

  • LocalVariableTypeTable

  • SourceDebugExtension

  • SourceFile

JVM 未使用的属性包括:

  • AnnotationDefault

  • EnclosingMethod

  • InnerClasses

  • MethodParameters

  • RuntimeInvisibleAnnotations

  • RuntimeInvisibleParameterAnnotations

  • RuntimeInvisibleTypeAnnotations

  • RuntimeVisibleAnnotations

  • RuntimeVisibleParameterAnnotations

  • RuntimeVisibleTypeAnnotations

  • Signature

  • Synthetic

在 CDS 存档中存储内部字符串[JEP 250]

存储和访问字符串到和从类数据共享CDS)存档的方法效率低下,耗时过多,且浪费内存。以下图表说明了 Java 在 CDS 存档中存储内部字符串的方法:

低效源于当前的存储架构。特别是当 类数据共享 工具将类倾倒到共享存档文件中时,包含 CONSTANT_String 项的常量池具有 UTF-8 字符串表示形式。

UTF-8 是一个 8 位可变长度字符编码标准。

问题

在当前使用 UTF-8 的情况下,字符串必须转换为字符串对象,即 java.lang.String 类的实例。这种转换是按需进行的,可能会导致系统变慢和不必要的内存使用。处理时间非常短,但内存使用不能忽视。每个内部字符串中的字符至少需要 3 字节内存,可能还需要更多。

一个相关问题是存储的字符串对所有 JVM 进程不可访问。

解决方案

CDS 存档现在在堆上为字符串分配特定空间:

字符串空间是通过共享字符串表、哈希表和去重来映射的。

去重是数据压缩技术,它可以消除存档中的重复信息。

为模块化准备 JavaFX UI 控件和 CSS API [JEP 253]

JavaFX 是一套允许设计和开发媒体丰富的图形用户界面的包。JavaFX 应用程序为开发者提供了一个创建一致界面的强大 API。层叠样式表CSS)可以用来自定义界面。JavaFX 的一个优点是编程和界面设计任务可以轻松分离。

JavaFX 概述

有一个叫做 Scene Builder 的出色的可视化脚本工具,它允许你通过拖放和属性设置来创建图形用户界面。Scene Builder 生成必要的 FXML 文件,这些文件被你的 集成开发环境IDE)如 NetBeans 所使用。

这里是一个使用 Scene Builder 创建的示例 UI:

此外,这是 Scene Builder 创建的 FXML 文件:

    <?xml version="1.0" encoding="UTF-8"?>

    <?import java.lang.*?>
    <?import java.util.*?>
    <?import javafx.scene.control.*?>
    <?import javafx.scene.layout.*?>
    <?import javafx.scene.paint.*?>
    <?import javafx.scene.text.*?>

    <AnchorPane id="AnchorPane" maxHeight="-Infinity"
     maxWidth="-Infinity" minHeight="-Infinity"
     minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0"

     >
     <children>
       <TitledPane animated="false" collapsible="false"
        layoutX="108.0" layoutY="49.0" text="Sample">
       <content>
         <AnchorPane id="Content" minHeight="0.0" minWidth="0.0"
          prefHeight="180.0" prefWidth="200.0">
         <children>
           <CheckBox layoutX="26.0" layoutY="33.0" 
            mnemonicParsing="false" prefWidth="94.0" 
            text="CheckBox" />
           <ColorPicker layoutX="26.0" layoutY="65.0" />
           <Hyperlink layoutX="26.0" layoutY="103.0"
            text="Hyperlink" />
           <Label alignment="CENTER" layoutX="14.0" layoutY="5.0" 
            prefWidth="172.0" text="This is a Label"
            textAlignment="CENTER">
            <font>
              <Font size="14.0" />
            </font>
           </Label>
           <Button layoutX="81.0" layoutY="146.0" 
            mnemonicParsing="false" text="Button" />
         </children>
         </AnchorPane>
       </content>
       </TitledPane>
     </children>
    </AnchorPane>

Java 9 的影响

在 Java 9 之前,JavaFX 控件以及 CSS 功能只能通过接口内部 API 由开发者访问。Java 9 的模块化使得内部 API 不可访问。因此,JEP 253 被创建来定义公共 API,而不是内部 API。

这是一项比看起来更大的任务。以下是一些作为此 JEP 部分采取的行动:

  • 将 javaFX 控件皮肤从内部移动到公共 API (javafx.scene.skin)

  • 确保 API 一致性

  • 生成详尽的 javadoc

以下类已从内部包移动到公共 javafx.scene.control.skin 包:

AccordionSkin ButtonBarSkin ButtonSkin CellSkinBase
CheckBoxSkin ChoiceBoxSkin ColorPickerSkin ComboBoxBaseSkin
ComboBoxListViewSkin ComboBoxPopupControl ContextMenuSkin DateCellSkin
DatePickerSkin HyperLinkSkin LabelSkin LabeledSkinBase
ListCellSkin ListViewSkin MenuBarSkin MenuButtonSkin
MenuButtonSkinbase NestedTableColumHeader PaginationSkin ProgressBarSkin
ProgressIndicatorSkin RadioButtonSkin ScrollBarSkin ScrollPaneSkin
SeparatorSkin SliderSkin SpinnerSkin SplitMenuButtonSkin
SplitPaneSkin TabPaneSkin TableCellSkin TableCellSkinBase
TableColumnHeader TableHeaderRow TableHeaderSkin TableRowSkinBase
TableViewSkin TableViewSkinBase TextAreaSkin TextFieldSkin
TextInputControlSkin TitledPaneSkin ToggleButtonSkin TooBarSkin
TooltipSkin TreeCellSkin TreeTableCellSkin TreeTableRowSkin
TreeTableViewSkin TreeViewSkin VirtualContainerBase VirtualFlow

公共的javafx.css包现在有额外的类:

  • CascadingStyle.java:public class CascadingStyle implements Comparable<CascadingStyle>

  • CompoundSelector.java:final public class CompoundSelector extends Selector

  • CssError.java:public class CssError

  • Declaration.java:final public class Declaration

  • Rule.java:final public class Rule

  • Selector.java:abstract public class Selector

  • SimpleSelector.java:final public class SimpleSelector extends Selector

  • Size.java:final public class Size

  • Style.java:final public class Style

  • Stylesheet.java:public class Stylesheet

  • CssParser.java:final public class CssParser

紧凑字符串 [JEP 254]

字符串数据类型是几乎所有 Java 应用程序的重要部分。虽然 JEP 254 的目标是使字符串更节省空间,但它采取了谨慎的态度,以确保现有的性能和兼容性不会受到负面影响。

Java 9 之前的状态

在 Java 9 之前,字符串数据存储为字符数组。每个字符需要 16 位。确定大多数 String 对象可以用仅 8 位,即 1 个字节的存储来存储。这是因为大多数字符串由拉丁-1 字符组成。

ISO Latin-1 字符集是一个单字节字符编码集。

Java 9 的新特性

从 Java 9 开始,字符串现在使用字节数组和编码引用的标志字段内部表示。

将选定的 Xerces 2.11.0 更新合并到 JAXP [JEP 255]

Xerces 是一个用于在 Java 中解析 XML 的库。它在 2010 年底更新到 2.11.0,因此 JEP 255 的目标是更新 JAXP 以包含 Xerces 2.11.0 的变化。

JAXP 是 Java 的 XML 处理 API。

在 Java 9 之前,JDK 关于 XML 处理的最新更新基于 Xerces 2.7.1。基于 Xerces 对 JDK 7 的一些额外更改,2.10.0。JEP 255 是基于 Xerces 2.11.0 的 JAXP 的进一步改进。

Xerces 2.11.0 支持以下标准:

  • XML 1.0,第四版

  • XML 1.0,第二版中的命名空间

  • XML 1.1,第二版

  • XML 1.1,第二版中的命名空间

  • XML 包含 1.0,第二版

  • 文档对象模型 (DOM)

    • 第 3 级

      • 核心

      • 加载 & 保存

    • 第 2 级

      • 核心

      • 事件

  • 遍历 & 范围

  • 元素遍历,第一版

  • 简单 XML API 2.0.2

  • Java XML 处理 API (JAXP) 1.4

  • XML 流式处理 API 1.0

  • XML 模式 1.0

  • XML 模式 1.1

  • XML 模式定义语言

JDK 已更新以包含以下 Xerces 2.11.0 类别:

  • 目录解析器

  • 数据类型

  • 文档对象模型第 3 级

  • XML 模式验证

  • XPointer

Java 9 中未更改 JAXP 的公共 API。

将 JavaFX/Media 更新到 GStreamer 的新版本 [JEP 257]

JavaFX 用于创建桌面和 Web 应用程序。JavaFX 是为了取代 Swing 作为 Java 的标准 GUI 库而创建的。Media 类,javafx.scene.media.Media,用于实例化表示媒体资源的对象。JavaFX/Media 指的是以下类:

    public final class Media extends java.lang.Object

此类提供对媒体资源的引用数据。javafx.scene.media 包为开发人员提供了将媒体集成到他们的 JavaFX 应用程序中的能力。JavaFX/Media 使用 GStreamer 管道。

GStreamer 是一个多媒体处理框架,可用于构建从多种不同格式获取媒体并处理后以选定格式导出的系统。

JEP 257 的目的是确保 JavaFX/Media 更新到 GStreamer 的最新版本,以确保稳定性、性能和安全保证。

HarfBuzz 字体布局引擎 [JEP 258]

在 Java 9 之前,布局引擎用于处理字体复杂性;具体来说,是指那些具有超出常见拉丁字体渲染行为的字体。Java 使用统一客户端界面,也称为 ICU,作为默认文本渲染工具。ICU 布局引擎已被弃用,并在 Java 9 中被 HarfBuzz 字体布局引擎所取代。

HarfBuzz 是一个 OpenType 文本渲染引擎。此类布局引擎具有提供脚本感知代码的特性,以帮助确保文本按预期布局。

OpenType 是一种 HTML 格式化字体格式规范。

从 ICU 布局引擎到 HarfBuzz 字体布局引擎的变更动力是 IBM 决定停止支持 ICU 布局引擎。因此,JDK 已更新以包含 HarfBuzz 字体布局引擎。

Windows 和 Linux 上的 HiDPI 图形 [JEP 263]

JEP 263 专注于确保屏幕组件的清晰度,相对于显示器的像素密度。以下术语与该 JEP 相关,并附带以下列出的描述信息:

  • DPI 可知应用程序:一个能够检测和缩放图像以适应显示特定像素密度的应用程序

  • DPI 不可知应用程序:一个不尝试检测和缩放图像以适应显示特定像素密度的应用程序

  • HiDPI 图形: 每英寸高点数图形

  • 视网膜显示器:这个术语由苹果公司创造,指的是至少每英寸 300 像素密度的显示器

向用户显示图形,无论是图像还是图形用户界面组件,通常具有至关重要的性能。以高质量显示这些图像可能有些问题。计算机监视器的 DPI 有很大的变化。开发显示器的三种基本方法如下:

  • 开发不考虑潜在不同显示尺寸的应用程序。换句话说,创建一个 DPI 无感知的应用程序。

  • 开发一个对 DPI 敏感的应用程序,该程序可以选择性地使用给定显示器的预渲染图像大小。

  • 开发一个对 DPI 敏感的应用程序,该程序能够正确地缩放图像以适应应用程序运行的特定显示器。

显然,前两种方法都有问题,并且原因不同。在第一种方法中,没有考虑用户体验。当然,如果应用程序是为具有无预期像素密度变化的特定显示器开发的,那么这种方法可能是可行的。

第二种方法需要在设计和开发端进行大量工作,以确保为每个预期的显示密度创建并程序化实现图像。除了大量的工作外,应用程序的大小将不必要地增加,并且没有考虑到新的和不同的像素密度。

第三种方法是为具有高效和有效缩放功能的应用程序创建一个对 DPI 敏感的应用程序。这种方法效果很好,并且已经在 Mac 视网膜显示器上得到证明。

在 Java 9 之前,自动缩放和尺寸调整已经在 Java 中实现,用于 Mac OS X 操作系统。这种功能在 Java 9 中添加到了 Windows 和 Linux 操作系统。

Marlin 图形渲染器 [JEP 265]

JEP 265 在 Java 2D API 中将 Pisces 图形光栅化器替换为 Marlin 图形渲染器。此 API 用于绘制 2D 图形和动画。

目标是用一个效率更高且没有质量损失的光栅化器/渲染器替换 Pisces。这个目标在 Java 9 中实现了。一个预期的附带好处是包括一个开发者可访问的 API。以前,与 AWT 和 Java 2D 接口的方式是内部的。

Unicode 8.0.0 [JEP 267]

Unicode 8.0.0 于 2015 年 6 月 17 日发布。JEP 267 专注于更新相关 API 以支持 Unicode 8.0.0。

Unicode 8.0.0 中的新内容

Unicode 8.0.0 增加了近 8000 个字符。以下是发布的高亮内容:

  • Ahom 语脚本(印度泰阿洪语)

  • Arwi,泰米尔语(阿拉伯语)

  • 切罗基符号

  • CJK 统一表意文字

  • 表情符号以及肤色符号修饰符

  • 格鲁吉亚拉里货币符号

  • lk 语言(乌干达)

  • Kulango 语言(科特迪瓦)

Java 9 中的更新类

为了完全符合新的 Unicode 标准,更新了几个 Java 类。以下列出的类在 Java 9 中更新,以符合新的 Unicode 标准:

  • java.awt.font.NumericShaper

  • java.lang.Character

  • java.lang.String

  • java.text.Bidi

  • java.text.BreakIterator

  • java.text.Normalizer

为关键部分预留栈区域 [JEP 270]

JEP 270 的目标是减轻在执行关键部分时由于栈溢出而产生的问题。这种缓解措施采取了预留额外线程栈空间的形式。

Java 9 之前的状况

当 JVM 被要求在一个没有足够栈空间且没有权限分配额外空间的线程中执行数据计算时,它会抛出StackOverflowError。这是一个异步异常。当方法被调用时,JVM 也可以同步地抛出StackOverflowError异常。

当方法被调用时,会使用一个内部过程来报告栈溢出。虽然当前的架构足以报告错误,但调用应用程序没有足够的空间轻松地从错误中恢复。这可能会给开发人员和用户带来更多的不便。如果在关键的计算操作期间抛出了StackOverflowError,数据可能会被损坏,导致更多的问题。

虽然这些问题的原因并非唯一,但ReentrantLock类的受影响状态是导致不理想结果的一个常见原因。这个问题在 Java 7 中很明显,因为ConcurrentHasMap代码实现了ReentrantLock类。ConcurrentHasMap代码在 Java 8 中进行了修改,但对于ReentrantLock类的任何实现,问题仍然存在。类似的问题超出了仅仅使用ReentrantLock类的情况。

以下图表提供了StackOverflowError问题的广泛概述:

在下一节中,我们将探讨如何解决 Java 9 中的这个问题。

Java 9 新增功能

在 Java 9 中,通过 JEP 270 的更改,关键部分将自动获得额外的空间,以便它可以完成其执行而不会遭受StackOverflowError。这基于额外的空间分配需求很小。JVM 已经进行了必要的更改,以允许此功能。

当 JVM 在执行关键部分时,实际上会延迟StackOverflowError,或者至少尝试这样做。为了利用这个新架构,方法必须使用以下注解:

    jdk.internal.vm.annotation.ReservedStackAccess

当一个方法有这个注解并且存在StackOverflowError条件时,会授予临时访问预留内存空间。这个新过程在高度抽象的层面上可以表示如下:

语言定义的对象模型的动态链接 [JEP 276]

通过 JEP 276 增强了 Java 互操作性。必要的 JDK 更改已做出,以允许来自多种语言的运行时链接器在单个 JVM 实例中共存。这个更改适用于高级操作,正如您所期望的。一个相关的高级操作示例是读取或写入具有访问器和修改器等元素的属性。

高级操作适用于未知类型的对象。它们可以使用INVOKEDYNAMIC指令调用。以下是一个在编译时未知对象类型时调用对象属性示例:

    INVOKEDYNAMIC "dyn:getProp:age"

概念证明

Nashorn 是一个轻量级、高性能的 JavaScript 运行时,允许在 Java 应用程序中嵌入 JavaScript。这是为 Java 8 创建的,并取代了基于 Mozilla Rhino 的先前 JavaScript 脚本引擎。Nashorn 已经具有这种功能。它提供任何未知类型对象上的高级操作之间的链接,例如obj.something,它产生以下结果:

    INVOKEDYNAMIC "dyn.getProp.something"

动态链接器立即启动,并在可能的情况下提供适当的实现。

G1 中巨大对象的附加测试 [JEP 278]

Java 平台长期受欢迎的功能之一是幕后垃圾回收。JEP 278 的焦点是创建 G1 垃圾回收器的附加 WhiteBox 测试,作为对巨大对象的特性。

WhiteBox 测试是一个用于查询 JVM 内部的 API。WhiteBox 测试 API 在 Java 7 中引入,并在 Java 8 和 Java 9 中进行了升级。

G1 垃圾回收器工作得非常好,但仍有提高效率的空间。G1 垃圾回收器的工作方式是首先将堆分成大小相等的区域,如下所示:

图片

G1 垃圾回收器的问题在于如何处理巨大对象。

在垃圾回收的上下文中,巨大对象是指占据堆上多个区域的对象。

巨大对象的问题在于,如果它们占据了堆上任何区域的一部分,剩余的空间就无法为其他对象分配。在 Java 9 中,WhiteBox API 通过四种新方法进行了扩展:

  • 用于阻止完全垃圾回收并启动并发标记的方法。

  • 可以访问单个 G1 垃圾回收器堆区域的方法。对这些区域的访问包括属性读取,例如区域的当前状态。

  • 直接访问 G1 垃圾回收器内部变量的方法。

  • 可以确定巨大对象是否位于堆上以及它们位于哪些区域的方法。

改进测试失败故障排除 [JEP 279]

对于做很多测试的开发者来说,JEP 279 值得一读。Java 9 中添加了额外的功能,以自动收集信息以支持故障排除测试失败以及超时。在测试期间收集可用的诊断信息,可以为开发者和工程师提供更精确的日志和其他输出。

在测试的上下文中,有两种基本类型的信息——环境和过程。

环境信息

在运行测试时,测试环境信息对于故障排除工作可能很重要。这些信息包括以下内容:

  • CPU 负载

  • 磁盘空间

  • I/O 负载

  • 内存空间

  • 打开的文件

  • 打开的套接字

  • 运行的进程

  • 系统事件

  • 系统消息

Java 进程信息

在测试过程中,还有与 Java 进程直接相关的信息。这些包括:

  • C 栈

  • 核心转储

  • 小型转储

  • 堆统计信息

  • Java 栈

有关此概念的更多信息,请阅读关于 JDK 回归测试工具(jtreg)的内容。

优化字符串连接 [JEP 280]

JEP 280 是 Java 平台的一个有趣增强。在 Java 9 之前,字符串连接被 javac 翻译成StringBuilder :: append链。这是一种次优的翻译方法,通常需要StringBuilder预分配大小。

增强改变了由 javac 生成的字符串连接字节码序列,使其使用INVOKEDYNAMIC调用。增强的目的是提高优化并支持未来的优化,而无需重新格式化 javac 的字节码。

有关INVOKEDYNAMIC的更多信息,请参阅 JEP 276。

使用INVOKEDYNAMIC调用java.lang.invoke.StringConcatFactory允许我们使用类似于 lambda 表达式的方法,而不是使用 StringBuilder 的逐步过程。这导致字符串连接处理更加高效。

HotSpot C++单元测试框架 [JEP 281]

HotSpot 是 JVM 的名称。这个 Java 增强旨在支持为 JVM 开发 C++单元测试。以下是此增强功能的部分、非优先级列表的目标:

  • 命令行测试

  • 创建适当的文档

  • 调试编译目标

  • 框架弹性

  • IDE 支持

  • 单独和隔离的单元测试

  • 个性化测试结果

  • 与现有基础设施集成

  • 内部测试支持

  • 正面和负面测试

  • 短时间执行测试

  • 支持所有 JDK 9 构建平台

  • 测试编译目标

  • 测试排除

  • 测试分组

  • 需要初始化 JVM 的测试

  • 与源代码位于同一位置的测试

  • 平台相关代码的测试

  • 编写和执行单元测试(对于类和方法)

这个增强功能是可扩展性增加的证据。

在 Linux 上启用 GTK 3 [JEP 283]

GTK+,正式称为 GIMP 工具箱,是一个跨平台工具,用于创建 图形用户界面GUI)。该工具由其 API 可访问的小部件组成。JEP 283 的重点是确保在开发具有图形组件的 Java 应用程序时,GTK 2 和 GTK 3 在 Linux 上得到支持。该实现支持使用 JavaFX、AWT 和 Swing 的 Java 应用程序。

我们可以使用 JavaFX、AWT 和 Swing 创建 Java 图形应用程序。以下表格总结了这三个方法与 GTK 的关系,在 Java 9 之前:

方法 备注
JavaFX
  • 使用动态 GTK 函数查找

  • 通过 JFXPanel 与 AWT 和 Swing 交互

  • 使用 AWT 打印功能

|

AWT
  • 使用动态 GTK 函数查找

|

Swing
  • 使用动态 GTK 函数查找

|

因此,为了实现这个 JEP,需要做出哪些改变?对于 JavaFX,有三个具体的变化:

  • 为 GTK 2 和 GTK 3 添加了自动测试

  • 添加了动态加载 GTK 2 的功能

  • 添加了对 GTK 3 的支持

对于 AWT 和 Swing,实施了以下更改:

  • 为 GTK 2 和 GTK 3 添加了自动测试

  • AwtRobot 已迁移到 GTK 3

  • FileChooserDilaog 已更新以兼容 GTK 3

  • 添加了动态加载 GTK 3 的功能

  • Swing GTK LnF 已修改以支持 GTK 3

Swing GTK LnF 是 Swing GTK look and feel 的缩写。

新的 HotSpot 构建系统 [JEP 284]

在 Java 9 之前,Java 平台使用的构建系统充满了重复代码、冗余和其他低效之处。基于 build-infra 框架,构建系统已被重新设计以适应 Java 9。在此上下文中,infra 是基础设施的缩写。JEP 284 的总体目标是升级构建系统,使其简化。具体目标包括:

  • 利用现有的构建系统

  • 可维护的代码

  • 最小化重复代码

  • 简化

  • 支持未来增强

您可以在本网站上了解更多关于 Oracle 基础设施框架的信息:www.oracle.com/technetwork/oem/frmwrk-infra-496656.html

摘要

在本章中,我们介绍了 Java 平台的一些令人印象深刻的新特性,特别关注 javac、JDK 库和各种测试套件。内存管理改进,包括堆空间效率、内存分配和改进的垃圾收集,代表了一组强大的 Java 平台增强。关于编译过程的改变,以提高效率,也是我们章节的一部分。我们还介绍了重要的改进,如编译过程、类型测试、注解和自动运行时编译器测试。

在下一章中,我们将探讨 Java 9 中引入的几个小的语言增强。

第三章:Java 9 语言增强

在上一章中,我们了解了 Java 9 中包含的一些令人兴奋的新特性。我们的重点是 javac、JDK 库和测试套件。我们了解了内存管理改进,包括内存分配、堆优化和增强的垃圾回收。我们还涵盖了编译过程、类型测试、注解和运行时编译器测试的变更。

本章涵盖了 Java 9 中对变量处理器、弃用警告、Java 7 中实现的 Project Coin 变更的改进以及导入语句处理的一些变更。这些变更代表了 Java 语言本身的变更。

我们将在这里讨论的主题包括:

  • 变量处理器

  • 导入语句弃用警告

  • Project Coin

  • 导入语句处理

使用变量处理器 [JEP 193]

变量处理器是变量的类型化引用,并由 java.lang.invoke.VarHandle 抽象类管理。VarHandle 方法的签名是多态的。这为方法签名和返回类型提供了极大的可变性。以下是一个代码示例,演示了如何使用 VarHandle

    . . . 

    class Example 
    {
      int myInt;
      . . . 
    }
    . . . 
    class Sample 
    {
      static final VarHandle VH_MYINT;

      static 
      {
        try 
        {
          VH_MYINT =  
            MethodHandles.lookup().in(Example.class)
            .findVarHandle(Example.class, "myInt", int.class);
        } 
        catch (Exception e) 
        {
          throw new Error(e);
        }
      }
    }

    . . . 

如前述代码片段所示,VarHandle.lookup() 执行的操作与 MethodHandle.lookup() 方法执行的操作相同。

本 JEP 的目标是标准化以下类的方法调用方式:

  • java.util.concurrent.atomic

  • sun.misc.Unsafe

特别是,以下类的方法调用方式:

  • 访问/修改对象字段

  • 访问/修改数组元素

此外,本 JEP 导致了两个用于内存排序和对象可达性的栅栏操作。本着尽职尽责的精神,特别关注确保 JVM 的安全性。重要的是要确保这些变更不会导致内存错误。数据完整性、可用性和当然,性能是上述尽职尽责的关键组成部分,以下将进行解释:

  • 安全性:必须不可能出现损坏的内存状态。

  • 数据完整性:确保访问对象字段的规则与以下规则相同:

    • getfield 字节码

    • putfield 字节码

  • 可用性:可用性的基准是 sun.misc.Unsafe API。目标是使新 API 比基准更容易使用。

  • 性能:与使用 sun.misc.Unsafe API 相比,性能不能下降。目标是超越该 API。

在 Java 中,栅栏操作是 javac 强制对内存施加约束的形式,这种约束以屏障指令的形式出现。这些操作发生在屏障指令之前和之后,本质上是在这些操作周围设置栅栏。

使用 AtoMiC 工具包

java.util.concurrent.atomic 包包含 12 个子类,支持对线程安全和无锁的单个变量进行操作。在这个上下文中,线程安全指的是访问或修改共享单个变量的代码,而不会阻碍其他线程同时在该变量上执行。这个超类是在 Java 7 中引入的。

这里是 AtoMiC 工具包中 12 个子类的一个列表。正如你所期望的,类名具有自描述性:

原子子类
java.util.concurrent.atomic.AtomicBoolean
java.util.concurrent.atomic.AtomicInteger
java.util.concurrent.atomic.AtomicIntegerArray
java.util.concurrent.atomic.AtomicIntegerFieldUpdater<T>
java.util.concurrent.atomic.AtomicLong
java.util.concurrent.atomic.AtomicLongArray
java.util.concurrent.atomic.AtomicLongFieldUpdater<T>
java.util.concurrent.atomic.AtomicMarkableReference<V>
java.util.concurrent.atomic.AtomicReference<V>
java.util.concurrent.atomic.AtomicReferenceArray<E>
java.util.concurrent.atomic.AtomicReferenceFieldUpdater<T,V>
java.util.concurrent.atomic.AtomicStampedReference<V>

可变变量、字段和数组元素可以被并发线程异步修改。

在 Java 中,volatile 关键字用于通知 javac 工具从主内存中读取值、字段或数组元素,而不是将它们缓存。

这里有一个代码片段,演示了如何使用 volatile 关键字对一个实例变量进行操作:

    public class Sample 
    {
      private static volatile Sample myVolatileVariable; // a
       volatile instance variable

      public static Sample getVariable() // getter method
      {
        if (myVolatileVariable != null) 
        {
          return myVolatileVariable;
        }
        // this section executes if myVolatileVariable == null
        synchronized(Sample.class)
        {
          if (myVolatileVariable == null)
          {
            myVolatileVariable =  new Sample();
          }
        }
    }

使用 sun.misc.Unsafe

sun.misc.Unsafe 类,像其他 sun 类一样,并未官方文档化或支持。它曾被用来绕过 Java 内置的某些内存管理安全特性。虽然这可以被视为在代码中获得更多控制和灵活性的窗口,但它是一种糟糕的编程实践。

该类有一个单独的私有构造函数,因此无法轻松实例化该类的实例。所以,如果我们尝试使用 myUnsafe = new Unsafe() 实例化一个实例,在大多数情况下都会抛出 SecurityException。这个相对难以触及的类有超过 100 个方法,允许对数组、类和对象进行操作。以下是这些方法的简要示例:

数组 对象
arrayBaseOffset defineAnonymousClass allocateInstance
arrayIndexScale defineClass objectFieldOffset
ensureClassInitialized
staticFieldOffset

这里是 sun.misc.Unsafe 类方法的一个关于信息、内存和同步的次要分组:

信息 内存 同步
addressSize allocateMemory compareAndSwapInt
pageSize copyMemory monitorEnter
freeMemory monitorExit
getAddress putOrderedEdit
getInt tryMonitorEnter
putInt

sun.misc.Unsafe类在 Java 9 中被标记为删除。实际上,在编程行业中对此决策有一些反对意见。为了平息他们的担忧,该类已被弃用,但不会完全删除。可以向 JVM 发送一个特殊标志来利用原始 API。

在导入语句上省略弃用警告 [JEP 211]

这是一些 Java 9 中较为简单的 JEP 之一。我们经常在编译程序时接收到许多警告和错误。编译器错误必须修复,因为它们通常是语法性的。另一方面,警告应该被审查并适当处理。一些警告消息被开发者忽略。

这个 JEP 在接收到警告的数量上提供了一些缓解。具体来说,由于导入语句引起的弃用警告不再生成。在 Java 9 之前,我们可以使用以下注解来抑制弃用警告消息:

    @SupressWarnings

现在,随着 Java 9 的到来,编译器将在以下情况之一为真时抑制弃用警告:

  • 如果使用了@Deprecated注解

  • 如果使用了@SuppressWarnings注解

  • 如果生成警告的代码的使用和声明在祖先类内

  • 如果生成警告的代码的使用在导入语句内

列出的第四个条件是在 Java 9 中添加的。

磨削项目硬币 [JEP 213]

Project Coin 是在 Java 7 中引入的较小更改的功能集。以下列出了这些更改:

  • switch语句中的字符串

  • 二进制整型文字

  • 在数字文字中使用下划线

  • 实现多捕获

  • 允许更精确地重新抛出异常

  • 泛型实例创建改进

  • 添加了try-with-resources语句

  • 调用varargs方法的改进

详细信息可以在以下 Oracle 演示文稿中找到:www.oracle.com/us/technologies/java/project-coin-428201.pdf

JEP 213 专注于对 Project Coin 增强功能的改进。共有五个这样的增强,如下详细说明。

使用@SafeVarargs 注解

在 Java 9 中,我们可以使用@SafeVarargs注解与私有实例方法。当我们使用这个注解时,我们正在断言该方法不包含对作为方法参数传递的varargs执行任何有害操作。

使用的语法如下:

    @SafeVarargs // this is the annotation
    static void methodName(...) 
    {

      /*
      The contents of the method or constructor must not 
      perform any unsafe or potentially unsafe operations 
      on the varargs parameter or parameters.
      */

    }

@SafeVarargs注解的使用限制为:

  • 静态方法

  • 最终实例方法

  • 私有实例方法

try-with-resource 语句

在使用最终变量时,try-with-resource语句之前需要为语句中的每个资源声明一个新变量。以下是 Java 9 之前(在 Java 7 或 8)try-with-resource语句的语法:

    try ( // open resources ) 
    {
      // use resources
    } catch (// error) 
    {  // handle exceptions
    }
    // automatically close resources

这里是一个使用上述语法的代码片段:

    try ( Scanner xmlScanner = new Scanner(new File(xmlFile));
    {
       while (xmlScanner.hasNext())
       {
          // read the xml document and perform needed operations
       }
      xmlScanner.close();
    } catch (FileNotFoundException fnfe)
      {
         System.out.println("Your XML file was not found.");
      }

现在,随着 Java 9 的推出,try-with-resource语句可以管理 final 变量,而无需声明新变量。因此,我们现在可以重写之前的代码,如下所示,在 Java 9 中:

    Scanner xmlScanner = new Scanner(newFile(xmlFile));
    try ( while (xmlScanner.hasNext())
    {
       {
         // read the xml document and perform needed operations
       }
       xmlScanner.close();
    } catch (FileNotFoundException fnfe)
      {
         System.out.println("Your XML file was not found.");
      }

如您所见,xmlScanner对象引用包含在try-with-resource语句块中,这提供了自动资源管理。资源将在try-with-resource语句块退出时自动关闭。

您还可以将finally块作为try-with-resource语句的一部分使用。

使用菱形运算符

在 Java 9 中引入的菱形运算符,如果推断的数据类型是可表示的,则可以与匿名类一起使用。当推断数据类型时,这表明 Java 编译器可以确定方法调用中的数据类型。这包括声明和任何包含的参数。

菱形运算符是小于和大于符号对(<>)。它对 Java 9 来说并不新鲜;相反,与匿名类的特定使用才是。

菱形运算符是在 Java 7 中引入的,使得实例化泛型类变得更加简单。以下是一个 Java 7 之前的示例:

    ArrayList<Student> roster = new ArrayList<Student>();

然后,在 Java 7 中,我们可以这样重写:

    ArrayList<Student> roster = new ArrayList<>();

问题在于这个方法不能用于匿名类。以下是一个 Java 8 中的示例,它运行良好:

    public interface Example<T> 
    {
      void aMethod()
      {
        // interface code goes here
      }
    }

    Example example = new Example<Integer>() 
    {
      @Override
      public void aMethod() 
      {
        // code
      }
    };

虽然前面的代码运行良好,但当我们将其更改为使用菱形运算符,如这里所示时,将发生编译器错误:

    public interface Example<T> 
    {
      void aMethod()
      {
        // interface code goes here
      }
    }

    Example example = new Example<>() 
    {
      @Override
      public void aMethod() 
      {
        // code
      }
    };

错误是由使用菱形运算符和匿名内部类引起的。Java 9 来拯救。虽然前面的代码在 Java 8 中会导致编译时错误,但在 Java 9 中运行良好。

停止使用下划线

下划线字符(_)不能再用作合法的标识符名称。之前尝试从标识符名称中删除下划线的尝试是不完整的。这样的使用将生成错误和警告的组合。在 Java 9 中,这些警告现在是错误。考虑以下示例代码:

    public class Java9Tests 
    { 
      public static void main(String[] args) 
      {
        int _ = 319;
        if ( _ > 300 )
        {
          System.out.println("Your value us greater than 300."); 
        } 
        else 
        {
          System.out.println("Your value is not greater than 300.");
        }
      }
    }

在 Java 8 中,前面的代码将对int _ = 319;if (_ > 300)语句产生编译器警告。警告是自版本 9 起,_是一个关键字,不能用作标识符。因此,在 Java 9 中,您将无法使用下划线本身作为合法的标识符。

被认为是不良的编程实践,使用不具有自我描述性的标识符名称。因此,仅使用下划线字符作为标识符名称不应是一个有问题的更改。

利用私有接口方法

Lambda 表达式是 Java 8 发布的一个重要部分。作为该改进的后续,接口中的私有方法现在可行。以前,我们无法在接口的非抽象方法之间共享数据。随着 Java 9 的推出,这种数据共享成为可能。接口方法现在可以是私有的。让我们看看一些示例代码。

这个第一个代码片段是我们在 Java 8 中编写接口的方式:

    . . . 
    public interface characterTravel
    {
      pubic default void walk()
      {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter desired pacing: ");
        int p = scanner.nextInt();
        p = p +1;
      }
      public default void run()
      {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter desired pacing: ");
        int p = scanner.nextInt();
        p = p +4;
      }
      public default void fastWalk()
      {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter desired pacing: ");
        int p = scanner.nextInt();
        p = p +2;
      }
      public default void retreat()
      {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter desired pacing: ");
        int p = scanner.nextInt();
        p = p - 1;
      }
      public default void fastRetreat()
      {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter desired pacing: ");
        int p = scanner.nextInt();
        p = p - 4;
      }
    }

现在,在 Java 9 中,我们可以重写这段代码。正如您接下来可以看到的,冗余的代码已被移动到单个名为 characterTravel 的私有方法中:

    . . . 
    public interface characterTravel
    {
      pubic default void walk()
      {
        characterTravel("walk");
      }
      public default void run()
      {
        characterTravel("run");
      }
      public default void fastWalk()
      {
        characterTravel("fastWalk");
      }
      public default void retreat()
      {
        characterTravel("retreat");
      }
      public default void fastRetreat()
      {
        characterTravel("fastRetreat");
      }
      private default void characterTravel(String pace)
      {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Enter desired pacing: ");
        int p = scanner.nextInt();
        if (pace.equals("walk"))
        {
          p = p +1;
        }
        else if (pace.equals("run"))
        {
          p = p + 4;
        }
        else if (pace.equals("fastWalk"))
        {
          p = p + 2;
        }
        else if (pace.equals("retreat"))
        {
          p = p - 1;
        }
        else if (pace.equals("fastRetreat"))
        {
          p = p - 4;
        }
        else
        {
          //
        }

正确处理导入语句 [JEP 216]

JEP 216 作为对 javac 处理导入语句的修复而发布。在 Java 9 之前,存在一些情况下导入语句的顺序会影响源代码是否被接受。

当我们在 Java 中开发应用程序时,我们通常会根据需要添加导入语句,从而形成一个无序的导入语句列表。IDEs 在对未使用的导入语句进行着色编码以及通知我们所需但尚未包含的导入语句方面做得很好。导入语句的顺序无关紧要;没有适用的层次结构。

javac 以两个主要步骤编译类。对于处理导入语句,这些步骤是类型解析和成员解析。类型解析包括对抽象语法树的审查,以识别类和接口的声明。成员解析包括确定类层次结构和单个类变量和成员。

在 Java 9 中,我们在类和文件中列出导入语句的顺序将不再影响编译过程。让我们来看一个例子:

    package samplePackage;

    import static SamplePackage.OuterPackage.Nested.*;
    import SamplePackage.Thing.*;

    public class OuterPackage 
    {
      public static class Nested implements Inner 
      { 
        // code
      }
    }

    package SamplePackage.Thing;

    public interface Inner 
    {
      // code
    }

在前面的例子中,类型解析发生,并得出以下认识:

  • SamplePackage.OuterPackage 存在

  • SamplePackage.OuterPackage.Nested 存在

  • SamplePackage.Thing.Innner 存在

下一步是成员解析,这是 Java 9 之前存在的问题所在。以下是 javac 对我们的示例代码进行成员解析的顺序步骤概述:

  1. SamplePackage.OuterPackage 的解析开始。

  2. SamplePackage.OuterPackage.Nested 的导入被处理。

  3. SamplePackage.Outer.Nested 类的解析开始。

  4. 内部接口进行了类型检查,尽管如此,由于它目前不在作用域内,因此内部无法解析。

  5. SamplePackage.Thing 的解析开始。此步骤包括将 SamplePackage.Thing 的所有成员类型导入作用域。

因此,在我们的例子中,错误发生是因为在尝试解析时 Inner 不在作用域内。如果步骤 4 和 5 互换,则不会出现问题。

Java 9 中解决问题的方案是将成员解析步骤分解为额外的子步骤。以下是这些步骤:

  1. 分析导入语句。

  2. 创建层次结构(类和接口)。

  3. 分析类头和类型参数。

摘要

在本章中,我们介绍了 Java 9 中关于变量处理器及其与 Atomic Toolkit 相关性的变化。我们还介绍了弃用警告及其在特定情况下被抑制的原因。还回顾了 Project Coin 的一部分,即 Java 7 引入的五个变化增强。最后,我们探讨了导入语句处理方面的改进。

在下一章中,我们将探讨由 Project Jigsaw 规定的 Java 模块结构。我们将深入探讨 Project Jigsaw 作为 Java 平台一部分的实现方式。本章中使用了来自一个示例电子商务应用的代码片段,以展示 Java 9 的模块化系统。同时,还讨论了 Java 平台在模块化系统方面的内部变化。

第四章:使用 Java 9 构建模块化应用程序

在上一章中,我们介绍了 Java 9 中关于变量处理器的变化以及它们与 AtoMiC Toolkit 的关系。我们还介绍了弃用警告以及为什么在特定情况下它们现在被抑制。我们还回顾了 Java 7 中Project Coin引入的变化的五个增强功能。最后,我们探讨了导入语句处理的改进。

在本章中,我们将检查由Project Jigsaw指定的 Java 模块结构。我们将深入了解Project Jigsaw作为 Java 平台一部分的实现方式。我们还将回顾与模块化系统相关的 Java 平台的关键内部变化。

我们将在这里讨论的主题包括:

  • Java 模块化简介

  • Java 平台模块系统的回顾

  • 模块化 JDK 源代码

  • 模块化运行时镜像

  • 了解模块化系统

  • 模块化 Java 应用程序打包

  • Java 链接器

  • 内部 API 的封装

模块化入门

在我们深入本章中 Java 9 的增强功能之前,让我们先考察一下在 Java 环境中模块化的含义。

我们可以将术语模块化定义为一种设计或构建类型,在我们的上下文中,是计算机软件的设计。这种类型的软件设计涉及一组模块,这些模块共同构成了整体。例如,一栋房子可以作为一个单一的结构建造,或者以模块化的方式建造,其中每个房间都是独立建造的,然后连接起来形成一个家。通过这个类比,你可以在创建你的家时选择性地添加或不添加模块。在我们的类比中,模块的集合变成了你家的设计。你的设计不需要使用每个模块,只需要你想要的那些。所以,例如,如果有地下室和奖励室模块,而你的设计不包括这些模块化的房间,那么这些模块就不会用来建造你的家。另一种选择是每个家都包括每个房间,而不仅仅是那些被使用的房间。这当然是浪费的。让我们看看这如何与软件相关联。

这个概念可以应用于计算机架构和软件系统。我们的系统可以由几个组件组成,而不是一个庞大的系统。正如你可能想象的那样,这为我们提供了一些特定的好处:

  • 我们应该能够扩展我们的 Java 应用程序以在小型设备上运行

  • 我们的 Java 应用程序将更小

  • 我们的可模块化代码可以更加具有针对性

  • 面向对象编程模型的使用增加

  • 增加封装的机会

  • 我们的代码将更高效

  • Java 应用程序将具有更高的性能

  • 整体系统复杂性降低

  • 测试和调试更容易

  • 代码维护更容易

由于几个原因,Java 转向模块化系统是必要的。以下是 Java 9 平台创建模块化系统的主要条件:

  • Java 开发工具包JDK)过于庞大。这使得支持小型设备变得困难。即使在下一节中讨论的紧凑配置文件中,支持某些小型设备也最多是困难的,在某些情况下甚至是不可能的。

  • 由于 JDK 过大,难以支持 Java 应用程序的真正优化性能。在这种情况下,越小越好。

  • Java 运行时环境JRE)太大,无法有效地测试和维护我们的 Java 应用程序。这导致耗时、低效的测试和维护操作。

  • Java 归档JAR)文件也太大。这使得支持小型设备变得有问题。

  • 由于 JDK 和 JRE 都是全面的,安全性是一个很大的担忧。例如,由于公共访问修饰符的性质,即使 Java 应用程序没有使用,内部 API 仍然可用。

  • 最后,我们的 Java 应用程序是不必要地大的。

模块化系统有以下要求:

  • 必须有一个公共接口以允许所有连接的模块之间的互操作性

  • 必须支持隔离和连接测试

  • 编译时操作必须能够识别正在使用的模块

  • 模块运行时支持

模块是 Java 9 中的一个新概念和组件;它是一组命名的数据和代码。具体来说,模块是一组:

  • 接口

  • 代码

  • 数据

  • 资源

成功实施的关键是,Java 9 中的模块在其模块声明中自我描述。模块名称必须是唯一的,通常使用反向域名模式。以下是一个示例声明:

    module com.three19.irisScan { }

模块声明包含在一个名为 module-info.java 的文件中,该文件应位于模块的根目录中。正如人们所期望的,此文件被编译成一个 module-info.class 文件,并将放置在适当的输出目录中。这些输出目录是在模块源代码中建立的。

在接下来的几节中,我们将探讨 Java 9 在模块化方面的具体变化。

审查 Java 的平台模块系统 [JEP-200]

JEP-200 的核心目标是使用Java 平台模块系统JPMS)对Java 开发工具包JDK)进行模块化。在 Java 9 之前,我们对 JDK 的了解包括对其主要组件的认识:

  • Java 运行时环境(JRE)

  • 解释器(Java)

  • 编译器(javac)

  • 归档器(jar)

  • 文档生成器(javadoc)

将 JDK 模块化的任务是将其分解成可以在编译时或运行时组合的组件。模块结构基于以下模块配置文件,这些配置文件在 Java 8 中作为紧凑配置文件建立。以下表格详细说明了这三个配置文件:

紧凑配置文件 1

java.io java.lang.annotation java.lang.invoke
java.lang.ref java.lang.reflect java.math
java.net java.nio java.nio.channels
java.nio.channels.spi java.nio.charset java.nio.charset.spi
java.nio.file java.nio.file.attribute java.nio.file.spi
java.security java.security.cert java.security.interfaces
java.security.spec java.text java.text.spi
java.time java.time.chrono java.time.format
java.time.temporal java.time.zone java.util
java.util.concurrent java.util.concurrent.atomic java.util.concurrent.locks
java.util.function java.util.jar java.util.logging
java.util.regex java.util.spi java.util.stream
java.util.zip javax.crypto javax.crypto.interfaces
javax.crypto.spec javax.net javax.net.ssl
javax.script javax.security.auth javax.security.auth.callback
javax.security.auth.login javax.security.auth.spi javax.security.auth.spi
javax.security.auth.x500 javax.security.cert

紧凑配置文件 2:

java.rmi java.rmi.activation java.rmi.dgc
java.rmi.registry java.rmi.server java.sql
javax.rmi.ssl javax.sql javax.transaction
javax.transaction.xa javax.xml javax.xml.database
javax.xml.namespace javax.xml.parsers javax.xml.stream
javax.xml.stream.events javax.xml.stream.util javax.xml.transform
javax.xml.transform.dom javax.xml.transform.sax javax.xml.transform.stax
javax.xml.transform.stream javax.xml.validation javax.xml.xpath
org.w3c.dom org.w3c.dom.bootstrap org.w3c.dom.events
org.w3c.dom.ls org.xml.sax org.xml.sax.ext
org.xml.sax.helpers

紧凑配置文件 3:

java.lang.instrument java.lang.management java.security.acl
java.util.prefs javax.annotation.processing javax.lang.model
javax.lang.model.element javax.lang.model.type javax.lang.model.util
javax.management javax.management.loading javax.management.modelmbean
javax.management.monitor javax.management.openmbean javax.management.relation
javax.management.remote javax.management.remote.rmi javax.management.timer
javax.naming javax.naming.directory javax.naming.event
javax.naming.ldap javax.naming.spi javax.security.auth.kerberos
javax.security.sasl javax.sql.rowset javax.sql.rowset.serial
javax.sql.rowest.spi javax.tools javax.xml.crypto
javax.xml.crypto.dom javax.xml.crypto.dsig javax.xml.crypto.dsig.dom
javax.xml.crypto.dsig.keyinfo javax.xml.crypto.dsig.spec org.ieft.jgss

Java 9 中标准模块化系统的基本由三个紧凑模块配置文件表示。这种标准化的有效性依赖于以下六个原则:

  • 所有由 JCP 管理的模块都必须以字符串 java. 开头。因此,如果一个空间实用工具模块正在开发中,它将有一个类似于 java.spatial.util 的名称。

JCP指的是Java 社区进程。JCP 允许开发者为 Java 创建技术规范。您可以在官方 JCP 网站上了解更多关于 JCP 的信息并成为其会员--www.jcp.org

  • 非 JCP 模块被视为 JDK 的一部分,并且它们的名称必须以字符串jdk.开头。

  • 确保方法调用链正确工作。以下流程图可以最好地说明这一点:

图片

如您在前面的流程图中所见,它仅适用于导出包的模块。

  • 第四个原则涉及在标准模块中使用标准和非标准 API 包。以下流程图说明了该原则的约定实施:

图片

  • 第五个设计原则是标准模块可以依赖于多个非标准模块。虽然这种依赖是被允许的,但对非标准模块的隐含可读性访问是不允许的。

  • 最终的设计原则确保非标准模块不导出标准 API 包。

模块化 JDK 源代码 [JEP-201]

如前所述,Project Jigsaw 的目标是模块化。预期的标准模块化系统将应用于 Java SE 平台和 JDK。除了效率提升外,模块化转型还将带来更好的安全性和易于维护性。JEP-201 中详细描述的增强主要集中在 JDK 源代码重组上。让我们更深入地了解一下。

重组 JDK 的源代码是一项重大任务,并以下列目标集完成:

  • 为 JDK 开发者提供对新的 Java 9 模块化系统的见解和熟悉度。因此,这个目标针对的是 JDK 的开发者,而不是主流开发者。

  • 确保在整个 JDK 构建过程中建立并维护模块边界。这是一项必要的预防措施,以确保模块化系统在 Java 9 的增强过程中以及更具体地,在实施模块化系统时保持稳定。

  • 第三个目标是确保未来的增强,特别是与Project Jigsaw相关的增强能够轻松地集成到新的模块化系统中。

这次源代码重组的重要性不容小觑。Java 9 之前的源代码组织已有 20 年历史。这次久拖不决的 JDK 源代码重组将使代码维护变得更加容易。让我们先看看 JDK 源代码的先前组织结构,然后再探讨其中的变化。

Java 9 之前的 JDK 源代码组织

JDK 是一系列代码文件、工具、库等内容的集合。以下插图提供了 JDK 组件的概述:

图片

在前一幅图示中,Java 9 之前的 JDK 组件组织结构将在接下来的七个子节中详细介绍。

开发工具

开发工具位于 \bin 目录中。这些工具包括七个广泛的分类,每个分类在后续章节中详细说明。

部署

这是一套旨在帮助部署 Java 应用程序的工具:

  • appletviewer: 这个工具允许你在不需要网络浏览器的情况下运行和调试 Java 小程序。

  • extcheck: 这个工具允许你查找 JAR 文件中的冲突。

  • jar: 这个工具用于创建和操作 JAR 文件。JAR 文件是 Java 归档文件。

  • java: 这是 Java 应用程序启动器。

  • javac: 这是 Java 编译器。

  • javadoc: 这个工具生成 API 文档。

  • javah: 这个工具允许你编写本地方法;它生成 C 头文件。

  • javap: 这个工具可以反汇编类文件。

  • javapackager: 用于签名和打包 Java 应用程序,包括 JavaFX。

  • jdb: 这是 Java 调试器。

  • jdeps: 这是一个 Java 类依赖分析器。

  • pack200: 这是一个将 JAR 文件压缩成 pack200 文件的工具。使用此工具的压缩比令人印象深刻。

  • unpack200: 这个工具解包 pack200 文件,生成 JAR 文件。

国际化

如果你感兴趣于创建可本地化的应用程序,以下工具可能会很有用:

  • native2ascii: 这个工具将普通文本转换为 Unicode Latin-1。

监控

用于提供 JVM 性能数据的监控工具包括:

  • jps: 这是 JVM 进程状态工具jps)。它提供特定系统上 HotSpot JVM 的列表。

  • jstat: 这是一个 JVM 统计监控工具。它从具有 HotSpot JVM 的机器上收集日志数据和性能信息。

  • jstatd: 这是 jstat 守护进程工具。它运行一个 RMI 服务器应用程序以监控 HotSpot JVM 操作。

RMI

RMI 工具是 远程方法调用 工具。它们帮助开发者创建在网络(包括互联网)上运行的应用程序:

  • rmic: 这个工具可以生成网络中对象的存根和骨架

  • rmiregistry: 这是一个远程对象的注册服务

  • rmid: 这个工具是 RMI 的激活系统守护进程

  • serialver: 这个工具返回 serialVersionUID 类值

安全

这套安全工具使开发者能够创建可以在开发者的计算机系统以及远程系统上实施的安全策略:

  • keytool: 这个工具管理安全证书和密钥库

  • jarsigner: 这个工具生成和验证 JAR 签名,用于创建/打开 JAR 文件

  • policytool: 这个工具具有图形用户界面,可以帮助开发者管理他们的安全策略文件

故障排除

这些实验性故障排除工具对于非常具体的故障排除非常有用。它们是实验性的,因此不受官方支持:

  • jinfo: 这个工具为特定进程、文件或服务器提供配置信息。

  • jhat: 这是一个堆转储工具。它实例化一个网络服务器,以便可以使用浏览器查看堆。

  • jmap:它显示进程、文件或服务器中的堆和共享对象内存映射。

  • jsadebugd:这是 Java 的服务性代理调试守护进程。它作为进程或文件的调试服务器。

  • jstack:这是一个 Java 堆栈跟踪工具,为进程、文件或服务器提供线程堆栈跟踪。

网络服务

这套工具提供了一个可以与Java Web Start和其他网络服务一起使用的实用程序:

  • javaws:这是一个命令行工具,用于启动 Java Web Start。

  • schemagen:这是一个用于生成 Java 架构模式的工具。这些模式用于 XML 绑定。

  • wsgen:这是一个用于生成可移植 JAX-WS 组件的工具。

  • wsimport:这是一个用于导入可移植 JAX-WS 组件的工具。

  • xjc:这是一个用于 XML 绑定的绑定编译器。

JavaFX 工具

JavaFX 工具位于几个不同的位置,包括\bin\man\lib目录。

Java 运行时环境

Java 运行时环境JRE)位于\jre目录中。关键内容包括Java 虚拟机JVM)和类库。

源代码

JDK 的源代码,在 Java 9 之前,具有以下基本组织架构:

    source code / [shared, OS-specific] / [classes / native] / Java API
     package name / [.file extension]

让我们更仔细地看看这个。在源代码之后,我们有两种选择。如果代码是跨平台的,那么它是一个共享目录;否则,它是特定于操作系统的。例如:

    src/share/...
    src/windows/...

接下来,我们有类目录或本地语言目录。例如:

    src/share/classes/...
    src/share/classes/java/...

接下来,我们有 Java API 包的名称,后面跟着文件扩展名。文件扩展名取决于内容,如.java.c等。

\lib目录包含\bin目录中一个或多个开发工具所需的类库。以下是典型 Java 8 \lib目录中的文件列表:

查看目录列表并不能提供很高的粒度洞察力。我们可以使用以下命令列出任何.jar文件中的类--jar tvf fileName.jar。以下是一个示例,展示了在命令行中执行jar tvf javafx-mx.jar生成的类列表:

C 头文件

/include目录包含 C 头文件。这些文件主要支持以下内容:

  • Java 本地接口JNI):用于本地代码编程支持。JNI 用于将 Java 本地方法和 JVM 嵌入到本地应用程序中。

  • JVM 工具接口JVM TI):由工具用于对运行 JVM 的应用程序进行状态检查和执行控制。

数据库

Apache Derby 关系数据库存储在/db目录中。您可以在以下网站了解更多关于 Java DB 的信息:

docs.oracle.com/javadb/support/overview.html

db.apache.org/derby/manuals/#docs_10.11

JDK 源代码重新组织

在前面的章节中,你了解到 Java 9 之前的源代码组织模式如下:

    source code / [shared, OS-specific] / [classes / native] / Java API 
     package name / [.file extension]

在 Java 9 中,我们有一个新的模块化模式。该模式如下:

    source code / module / [shared, OS-specific] / [classes / native / 
    configuration] / [ package / include / library ] /
     [.file extension]

新模式中存在一些差异,最显著的是模块名称。在共享或操作系统特定的目录之后,要么是类目录,要么是 C 或 C++源文件的本地目录,或者是一个配置目录。这种看似简单的组织模式改变导致代码库更加易于维护。

理解模块化运行时图像 [JEP-220]

Java 9 的模块化系统需要对运行时图像进行更改以实现兼容性。这些更改的好处包括以下方面的改进:

  • 可维护性

  • 性能

  • 安全性

这些变化的核心是一个用于资源命名的新的 URI 模式。这些资源包括模块和类。

统一资源标识符URI)与统一资源定位符URL)类似,因为它标识了某物的名称和位置。对于 URL,那是一个网页;对于 URI,它是一个资源。

JEP-220 有五个主要目标,以下各节将详细说明。

运行时格式采用

为 Java 9 创建了一种运行时格式,供存储类和其他资源文件采用。此格式适用于以下情况下的存储类和资源:

  • 当新的运行时格式比 Java 9 之前的 JAR 格式具有更高的效率(时间和空间)时。

JAR文件是一个Java ARchieve文件。这是一种基于传统 ZIP 格式的压缩文件格式。

  • 当存储类和其他资源可以单独隔离和加载时。

  • 当 JDK 和库类以及资源可以存储时。这包括应用程序模块。

  • 当它们以促进未来增强的方式设计时。这要求它们是可扩展的、有文档的并且灵活的。

运行时图像重构

Java 中有两种运行时图像类型--JDK 和 JRE。随着 Java 9 的推出,这两种图像类型都被重构,以区分用户可以使用和修改的文件与开发者及其应用程序可以使用的但不能修改的内部文件。

在 Java 9 之前,JDK 构建系统生成一个 JRE 和一个 JDK。JRE 是 Java 平台的完整实现。JDK 包括 JRE 以及其他工具和库。Java 9 的一个显著变化是 JRE 子目录不再属于 JDK 图像的一部分。这一变化部分是为了确保这两种图像类型(JDK 和 JRE)具有相同的图像结构。有了共同和重新组织过的结构,未来的更改将更加高效地集成。

如果你之前在 Java 9 之前创建了针对特定结构的自定义插件,那么你的应用程序可能在 Java 9 中无法工作。这也适用于你明确地引用tools.jar的情况。

以下图表提供了 Java 9 发布前每个图像内容的概览:

图片

下图展示了 Java 9 运行时映像。如图所示,一个完整的 JDK 映像包含与模块化运行时映像相同的目录,以及 demo、sample、man 和 includes 目录:

图片

现在不再有 JRE 或 JDK 映像之间的区别。现在,随着 Java 9 的推出,JDK 映像是一个包含完整开发工具的 JRE 映像。

支持常见操作

开发者有时必须编写执行需要访问运行时映像的操作的代码。Java 9 包括对这些常见操作的支持。这是由于对 JDK 和 JRE 运行时映像结构的重构和标准化而成为可能的。

取消 JDK 类的特权

Java 9 允许撤销单个 JDK 类的特权。这一变化加强了系统安全性,因为它确保 JDK 类只接收系统操作所需的权限。

保留现有行为

JEP-220 的最终目标是确保现有类不受负面影响。这指的是没有依赖内部 JDK 或 JRE 运行时映像的应用程序。

了解模块系统 [JEP-261]

本 JEP 的目的是实现 Java 平台的新模块系统。您会记得,模块化系统是为了为 Java 程序提供可靠的配置和强大的封装而创建的。这一实现的关键是链接时间概念。如图所示,链接时间是编译时间和运行时间之间可选的阶段。这个阶段允许将适当的模块组装成优化的运行时映像。这在一定程度上是由于 jlink 链接工具,您将在本章后面了解更多关于它的信息:

图片

模块路径

组织模块以便它们可以轻松定位是很重要的。模块路径,一系列模块组件或目录,提供了搜索使用的组织结构。这些路径组件按顺序搜索,返回第一个包含模块的路径组件。

模块及其路径不应被视为与包或类路径相同。它们确实是不同的,并且具有更高的保真度。关键区别在于,使用类路径时,搜索单个组件。模块路径搜索返回完整的模块。这种搜索可以通过以下路径进行,按此顺序搜索,直到返回一个模块:

  • 编译模块路径

  • 升级模块路径

  • 系统模块

  • 应用程序模块路径

让我们简要回顾一下这些路径。编译模块路径仅在编译时适用,包含模块定义。升级模块路径包含编译后的模块定义。系统模块是内置的,包括 Java SE 和 JDK 模块。最后一个路径,应用程序模块路径,包含来自应用程序模块以及库模块的编译后的模块定义。

访问控制边界违规

作为一名专业开发者,你总是希望你的代码是安全的、可移植的、无错误的,这需要严格遵守 Java 构造,如封装。在某些情况下,例如白盒测试,你需要打破 JVM 规定的封装。这个规定允许跨模块访问。

为了允许打破封装,你可以在你的模块声明中添加一个add-exports选项。以下是你会使用的语法:

    module com.three19.irisScan 
    {
      - - add-exports <source-module>/<package> = <target-module> 
      (, <target-module> )*
    }

让我们更详细地看看前面的语法。《source-module》和<target-module>是模块名称,<package>是包的名称。使用add-exports选项允许我们违反访问控制边界。

关于使用add-exports选项有两个规则:

  • 它可以在模块中使用多次

  • 每次使用都必须是<source-module><target-module>的唯一配对

不建议除非绝对必要,否则使用add-exports选项。它的使用允许对库模块的内部 API 进行危险访问。这种使用使得你的代码依赖于内部 API 不会改变,而这超出了你的控制范围。

运行时

HotSpot 虚拟机实现了jmodjlink命令行工具的<options>。以下是jmod命令行工具的<options>列表:

图片

这是jlink命令行工具的<options>列表:

图片

模块化 Java 应用程序打包 [JEP-275]

Java 9 的一个重大改进是Java Packager生成的运行时二进制文件的大小。这在很大程度上得益于下一节中将要介绍的Java 链接器。Java Packager 的工作流程在 Java 9 中基本上与 Java 8 相同。正如你将在本节后面看到的那样,工作流程中添加了新的工具。

Java Packager 仅创建 JDK 9 应用程序。这个对 Java Packager 的改变旨在简化并使生成运行时图像的过程更高效。因此,Java Packager 将只为它关联的 SDK 版本创建运行时图像。

高级查看 Java 链接器

在 Java 链接器工具 jlink 之前,Java 9 引入的运行时镜像创建包括复制整个 JRE。然后,删除未使用的组件。简单来说,jlink 促进了仅包含所需模块的运行时镜像的创建。jlink 被 Java 打包器用于生成嵌入式运行时镜像。

Java 打包器选项

Java 打包器的语法如下:

 javapackager -command [-options]

有五个不同的命令(-command)可以使用。具体描述如下:

command 描述
-createbss 此命令用于将文件从 CSS 转换为二进制
-createjar 此命令,结合其他参数,创建 JAR 归档文件
-deploy 此命令用于生成 jnlp 和 HTML 文件
-makeall 结合 -createjar-deploy 和编译步骤
-signJar 此命令用于创建并签名 JAR 文件

-createbss 命令的 [-options] 包括:

图片

-createjar 命令的 [-options] 包括:

图片

-deploy 命令的 [-options] 包括:

图片

这里是 -deploy 命令剩余的 [-options]

图片

-makeall 命令的 [-options] 包括:

图片

-signJar[-options] 包括:

图片

Java 打包器分为两个模块:

    jdk.packager
    jdk.packager.services

JLink - Java 链接器 [JEP-282]

Java 链接器,通常称为 JLink,是一个创建自定义运行时镜像的工具。此工具收集适当的模块及其依赖项,然后对它们进行优化以创建镜像。这代表了 Java 的一大变化,随着 Java 9 的发布。在 Java 链接器工具 jlink 可用之前,运行时镜像的创建最初包括复制整个 JRE。在后续步骤中,删除了未使用的组件。在 Java 9 中,jlink 创建仅包含所需模块的运行时镜像。jlink 被用于 Java 打包器以生成嵌入式运行时镜像。

如前文所述,JEP-282 导致链接时间成为编译时间和运行时间之间的一个可选阶段。在这一阶段,适当的模块被组装成一个优化的运行时镜像。

JLink 是一个命令行链接工具,允许创建包含 JDK 模块较小子集的运行时镜像。这导致运行时镜像更小。以下语法包括四个组件--jlink 命令、选项、模块路径和输出路径:

$ jlink <options> ---module-path <modulepath> --output <path>

这里是 jlink 工具可用的选项列表,以及每个选项的简要描述:

图片

模块路径告诉链接器在哪里找到模块。链接器将不会使用展开的模块或 JAR/JMOD 文件。

输出路径只是简单地通知链接器将自定义运行时镜像保存在哪里。

封装大多数内部 API [JEP-260]

JEP-260 的实施是为了使 Java 平台更加安全。这个 JEP 的核心目标是封装大多数内部 API。具体来说,JDK 的大多数内部 API 现在默认不再可访问。目前,被认为是关键广泛使用的内部 API 仍然可访问。在未来,我们可能会看到替代它们的功能,到那时,这些内部 API 将默认不可访问。

那么,为什么这个改动是必要的呢?有几个广泛使用的 API 不稳定,在某些情况下,还没有标准化。不支持的 API 不应该能够访问 JDK 的内部细节。因此,JEP-260 导致了 Java 平台安全性的提高。一般来说,您不应该在开发项目中使用不支持的 API。

上述关键 API(JDK 内部)如下:

  • sun.misc

  • sun.misc.Unsafe

  • sun.reflect.Reflection

  • sun.reflect.ReflectionFactory.newConstrutorForSerialization

上述关键内部 API 在 JDK 9 中仍然可访问。它们将通过 jdk.unsupported JDK 模块进行访问。完整的 JRE 和 JDK 镜像将包含 jdk.unsupported 模块。

您可以使用 Java 依赖分析工具 jdeps 来帮助确定您的 Java 程序是否依赖于 JDK 内部 API。

这是一个值得关注的有趣变化。很可能,当 Java 10 发布时,目前可访问的内部 API 将不会默认可访问。

摘要

在本章中,我们检查了由 Project Jigsaw 指定的 Java 模块结构,并深入探讨了 Project Jigsaw 如何实现以改进 Java 平台。我们还回顾了与模块化系统相关的 Java 平台的关键内部更改。我们的回顾从模块入门开始,我们学习了 Java 9 的模块化系统在优势和需求方面的内容。

我们探讨了 Java 9 如何将模块化引入 JDK,包括其源代码和组织结构。我们还探讨了构成 JDK 的七个主要工具类别。正如我们所学的,Java 9 的模块化也扩展到运行时镜像,从而提高了可维护性、性能和安全性。我们引入了链接时间的概念,作为编译时间和运行时间之间的可选阶段。我们以查看 Java 链接器和 Java 9 如何封装内部 API 来结束本章。

在下一章中,我们将探讨如何将现有应用程序迁移到 Java 9 平台。我们将探讨手动和半自动迁移过程。

第五章:将应用程序迁移到 Java 9

在上一章中,我们仔细研究了 Project Jigsaw 指定的 Java 模块结构,并检查了 Project Jigsaw 是如何实施以改进 Java 平台的。我们还回顾了 Java 平台的关键内部更改,特别关注新的模块化系统。我们从模块入门开始,了解了 Java 9 的模块化系统在优势和需求方面的内容。接下来,我们探讨了 Java 9 是如何将模块化引入 JDK 的。这包括对 Java 9 源代码重组的考察。我们还探讨了 JDK 的七个主要工具类别,并了解到 Java 9 的模块化扩展到运行时镜像,从而提高了可维护性、性能和安全性。我们引入了链接时间的概念,作为编译时间和运行时之间可选的阶段。我们以对Java 链接器的考察和 Java 9 如何封装内部 API 来结束本章。

在本章中,我们将探讨如何将现有应用程序迁移到 Java 9 平台。我们将查看手动和半自动迁移过程。Java 9 是一个重大版本,对 JDK 进行了许多更改,因此如果开发者的 Java 8 代码不再与 Java 9 兼容,开发者不应该感到惊讶。本章旨在为您提供洞察力和过程,以便使您的 Java 8 代码与 Java 9 兼容。

本章我们将涵盖以下主题:

  • 对 Project Jigsaw 的快速回顾

  • 模块如何在 Java 生态系统中定位

  • 迁移计划

  • 来自 Oracle 的建议

  • 有用的工具

对 Project Jigsaw 的快速回顾

Project Jigsaw 是涵盖对 Java 平台进行多项变更建议的 Java 项目。正如您在前面章节中读到的,Java 9 的最大变化涉及模块化和模块。将 Java 迁移到模块的倡议是由 Project Jigsaw 推动的。对模块化的需求源于 Java 的两个主要挑战:

  • Classpath

  • JDK

接下来,我们将回顾这两个挑战,并看看它们是如何在 Java 平台的新版本 Java 9 中得到解决和克服的。

Classpath

在 Java 9 之前,类路径存在问题,是开发者痛苦的来源。这在众多开发者论坛中很明显,幸运的是,Oracle 正在关注这个问题。以下是类路径可能存在问题的几个实例;以下是两个主要案例:

  • 第一种情况涉及在您的开发计算机上安装两个或更多版本的库。以前 Java 系统处理这种情况的方式并不一致。在类加载过程中使用哪个库是任何人都可以猜测的。这导致了不希望的不确定性——关于加载了哪个库的细节不足。

  • 第二种情况是在使用类加载器的最先进功能。很多时候,这种类加载器的使用导致了最多的错误和 bug。这些错误并不总是容易检测到,并为开发者带来了很多额外的工作。

在 Java 9 之前,类路径几乎总是非常长。Oracle 在最近的一次演讲中分享了一个包含 110 个 JAR 文件的类路径。这种难以管理的类路径使得检测冲突或确定是否缺少某些内容变得困难,如果缺少,可能缺少什么。将 Java 平台重新构想为模块系统使得这些类路径问题成为过去式。

模块通过提供可靠的配置解决了 Java 9 之前的类路径问题。

JDK 的单体性质

自 1995 年以来,Java 不断以令人印象深刻的方式发展,并且随着每一次的进化步骤,JDK 都变得越来越大。与 Java 8 类似,JDK 已经变得过于庞大。在 Java 9 之前,由于 JDK 的单体性质,存在一些问题,包括:

  • 由于 JDK 非常大,它不适合非常小的设备。在一些开发领域,这足以成为寻找非 Java 解决方案来解决软件工程问题的理由。

  • 过大的 JDK 导致了浪费。在设备、网络和云上运行时,在处理和内存方面都造成了浪费。这源于整个 JDK 都被加载,即使只需要 JDK 的一小部分。

  • 虽然 Java 平台在运行时具有出色的性能,但在启动性能方面,即加载和启动时间,还有很大的提升空间。

  • 数量庞大的内部 API 也一直是一个痛点。由于存在如此多的内部 API 并被开发者使用,系统难以进化。

  • 内部 API 的存在使得使 JDK 安全和可扩展变得困难。由于存在如此多的内部依赖,隔离安全和可扩展性问题变得过于复杂。

解决 JDK 单体问题的答案是模块。Java 9 引入了模块及其自己的模块系统。平台的一个重大更新是,只需编译所需的模块,而不是整个 JDK。这个模块系统在本书中进行了全面介绍。

模块通过提供强封装解决了 Java 9 之前 JDK 的单体问题。

模块如何融入 Java 生态系统

如您从以下插图中所见,包由类和接口组成,而模块由包组成。模块是包的容器。这是 Java 9 新模块系统的基本前提,从非常高的层面来看。重要的是要将模块视为模块系统的一部分,而不仅仅是包之上的一个新抽象级别,正如插图可能暗示的那样。

因此,模块是 Java 9 的新特性,正如你所预期的,在使用之前需要声明。模块的声明包括它所依赖的其他模块的名称。它还导出其他模块依赖的包。模块声明可能是你在开始使用 Java 9 开发时需要解决的最重要的问题。以下是一个示例:

    module com.three19.irisScan 
    {
      // modules that com.three19.irisScan depends upon
      requires com.three19.irisCore;
      requires com.three19.irisData;

      // export packages for other modules that are dependent
         upon com.three19.irisScan
      exports com.three19.irisScan.biometric;
    }

当编程 Java 9 应用程序时,你的模块声明将放置在 module-info.java 文件中。一旦完成此文件,你只需运行 javac,Java 编译器,以生成 module-info.class Java 类文件。你将以与当前编译 .java 文件到 .class 文件相同的方式完成此任务。

你还可以创建具有根目录中的 module-info.class 文件的模块 JAR 文件。这代表了一个很高的灵活性。

基础模块

当编程 Java 9 应用程序或移植使用旧版 Java 编写的现有应用程序时,必须使用基本模块 (java.base)。每个模块都需要 java.base 模块,因为它定义了关键的或基础性的 Java 平台 API。以下是 java.base 模块的内容:

    module java.base 
    {
      exports java.io;
      exports java.lang;
      exports java.lang.annotation;
      exports java.lang.invoke;
      exports java.lang.module;
      exports java.lang.ref;
      exports java.lang.reflect;
      exports java.math;
      exports java.net;
      exports java.net.spi;
      exports java.nio;
      exports java.nio.channels;
      exports java.nio.channels.spi;
      exports java.nio.charset;
      exports java.nio.charset.spi;
      exports java.nio.file;
      exports java.nio.file.attribute;
      exports java.nio.file.spi;
      exports java.security;
      exports java.security.aci;
      exports java.security.cert;
      exports java.security.interfaces;
      exports java.security.spec;
      exports java.text;
      exports java.text.spi;
      exports java.time;
      exports java.time.chrono;
      exports java.time.format;
      exports java.time.temporal;
      exports java.time.zone;
      exports java.util;
      exports java.util.concurrent;
      exports java.util.concurrent.atomic;
      exports java.util.concurrent.locks;
      exports java.util.function;
      exports java.util.jar;
      exports java.util.regex;
      exports java.util.spi;
      exports java.util.stream;
      exports java.util.zip;
      exports java.crypto;
      exports java.crypto.interfaces;
      exports java.crytpo.spec;
      exports java.net;
      exports java.net,ssi;
      exports java.security.auth;
      exports java.security.auth.callbak;
      exports java.security.auth.login;
      exports java.security.auth.spi;
      exports java.security.auth.x500;
      exports java.security.cert;
    }

如你所见,java.base 模块不需要任何模块,并且导出许多包。在开始使用新的 Java 平台,Java 9 创建应用程序时,有一个这些导出包的列表会很有用,这样你就知道可以使用什么了。

你会注意到,在前一节中,我们没有在我们的 com.three19.irisScan 模块声明中包含 requires java.base; 代码行。更新后的代码如下,并现在包含了 requires java.base; 代码行:

    module com.three19.irisScan 
    {
      // modules that com.three19.irisScan depends upon
      requires java.base; // optional inclusion 
      requires com.three19.irisCore;
      requires com.three19.irisData;

      // export packages for other modules that are dependent
         upon com.three19.irisScan
      exports com.three19.irisScan.biometric;
    }

如果你没有在模块声明中包含 requires java.base; 代码行,Java 编译器将自动包含它。

可靠配置

如本章前面所建议的,模块为我们提供了可靠的 Java 9 应用程序配置,解决了早期 Java 平台版本中的类路径问题。

Java 读取并解释模块声明,使得模块可读。这些可读的模块允许 Java 平台确定是否存在缺失的模块、是否有重复声明的库,或者存在其他冲突。在 Java 9 中,编译器或运行时将生成并输出非常具体的错误信息。以下是一个编译时错误的示例:

src/com.three19.irisScan/module-info.java: error: module not found: com.three19.irisScan 
requires com.three19.irisCore;
 ^
1 error

如果找不到模块 com.three19.isrisCorecom.three19.irisScan 应用程序需要它,将会发生以下运行时错误:

Error occurred during initialization of VM
java.lang.module.ResolutionException: Module com.three19.irisCore not found, required by com.three19.irisScan app

强封装

在本章前面,你了解到 Java 9 的强封装解决了单一代码库(JDK)的问题。在 Java 9 中,封装是由module-info.java文件中的信息驱动的。这个文件中的信息让 Java 知道哪些模块依赖于其他模块,以及每个模块导出了什么。这强调了确保我们的module-info.java文件正确配置的重要性。让我们看看一个用标准 Java 编写的示例,这种方式在 Java 9 中没有引入任何新内容:

图片

在前面的例子中,com.three19.irisScan模块有一个用于内部使用的irisScanner包和一个irisScanResult类。如果com.three19.access应用程序尝试导入和使用irisScanResult类,Java 编译器将产生以下错误信息:

src/com.three19.access/com/three19/access/Main.java: error: irisScanResult is not accessible because package com.three19.irisScanner.internal is not exported
 private irisSanResult scan1 = new irisScanResult();
 ^
1 error

如果出于某种原因编译器没有捕获这个错误,尽管这非常不可能,以下运行时错误将会发生:

Exception in thread "main" java.lang.IllegalAccessError: class com.three19.access.Main (in module: com.three19.access) cannot access class com.three19.irisScanner.internal.irisScanResult (in module: com.three19.irisScan), com.three19.irisScanner.internal is not exported to com.three19.access.

详细错误信息将使调试和故障排除变得更加容易。

迁移规划

Java 平台的变化是显著的,Java 9 被认为是一个重大版本。认为我们的当前 Java 应用程序将在 Java 9 上无缝工作是很天真的。虽然这可能成立,至少对于简单的程序来说,提前规划并考虑你可能会遇到的问题是很谨慎的。在我们查看这些问题之前,让我们在下一节测试一个简单的 Java 应用程序。

测试简单的 Java 应用程序

以下代码包含一个单一的 Java 类,GeneratePassword. 这个类会提示用户输入所需的密码长度,然后根据用户请求的长度生成密码。如果用户请求的长度小于 8,将使用默认长度 8。此代码是用 Java SE 1.7 JRE 系统库编写的:

    /*
    * This is a simple password generation app 
    */

    import java.util.Scanner;

    public class GeneratePassword 
    {
      public static void main(String[] args) 
      { 
        // passwordLength int set up to easily change the schema 
        int passwordLength = 8; //default value

        Scanner in = new Scanner(System.in); 
        System.out.println("How long would you like your
         password (min 8)?"); 
        int desiredLength; 
        desiredLength = in.nextInt(); 

        // Test user input 
        if (desiredLength >8) 
        { 
          passwordLength = desiredLength; 
        } 

        // Generate new password 
        String newPassword = createNewPassword(passwordLength); 

        // Prepare and provide output 
        String output = "\nYour new " + passwordLength 
         + "-character password is: "; 
        System.out.println(output + newPassword); 
      }

      public static String createNewPassword(int lengthOfPassword) 
      { 
        // Start with an empty String 
        String newPassword = "";

        // Populate password  
        for (int i = 0; i < lengthOfPassword; i++) 
        { 
          newPassword = newPassword + randomizeFromSet(
            "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ
             0123456789+-*/?!@#$%&"); 
        } 

        return newPassword; 
      }

      public static String randomizeFromSet(String characterSet) 
      { 
        int len = characterSet.length();
        int ran = (int)(len * Math.random());
        return characterSet.substring(ran, ran + 1);
      }
    }

在下面的屏幕截图,我们在运行 Java 8 的 Mac 上测试了GeneratePassword应用程序。如图所示,我们首先查询 Java 以验证当前版本。在这个测试中,使用了 Java 1.8.0_121。接下来,我们使用javac实用工具编译GeneratePassword Java 文件。最后,我们运行了应用程序:

图片

如前文测试所示,GeneratePassword.java 编译成功,生成了 GeneratePassword.class 文件。应用程序使用 java GeneratePassword 命令运行。用户被提示输入所需的密码长度,并输入了 32。然后应用程序成功生成了一个 32 位的随机密码,并提供了相应的输出。

这个测试演示了示例应用程序使用 JDK 1.8 成功运行。接下来,让我们使用 JDK 9 测试同一个应用程序。

我们从 java -version 命令开始,以表明我们在这台计算机上使用 JDK 9。以下截图显示我们成功将 .java 文件编译成 .class 文件。当应用程序运行时,它按预期工作并提供了正确的结果:

如你所见,我们清楚地证明了 Java 9 之前的应用程序有可能在无需进行任何修改的情况下成功运行在 Java 9 上。这是一个简单的案例研究,并展示了一个非常基础的 Java 程序。这当然是最好的情况,不能假设。你将想要测试你的应用程序,以确保它们在 Java 9 平台上按预期运行。

在下一节中,我们将回顾一些在使用带有 JDK 9 的新 Java 平台测试你的 Java 9 之前的应用程序时可能会遇到的问题。

潜在迁移问题

本节中提到的潜在迁移问题包括直接访问 JRE、访问内部 API、访问内部 JAR、JAR URL 废弃、扩展机制以及 JDK 的模块化。让我们逐一查看这些潜在的迁移问题。

JRE

创建 Java 9 的模块化系统在开发工具和实用工具的数量和位置上带来了一些简化。一个例子是 JDK 对 JRE 的消耗。在所有 Java 9 之前的版本中,Java 平台将 JDK 和 JRE 作为两个单独的组件包含在内。在 Java 9 中,这些组件已经被合并。这是一个重大的变化,开发者应该密切关注。如果你有一个指向 JRE 目录的应用程序,你需要进行更改以避免问题。JRE 内容如下所示:

访问内部 API

Java 9 平台已经封装了内部 API,以增加平台和用 Java 编写的应用程序的安全性。与 Java 平台之前的版本不同,你在 Java 9 中编写的应用程序将无法默认访问 JDK 的内部 API。Oracle 已经将一些内部 API 识别为关键;这些 API 通过 jdk.unsupported JDK 模块保持可访问。

上述关键 API(JDK 内部)包括:

  • sun.misc

  • sun.misc.Unsafe

  • sun.reflect.Reflection

  • sun.reflect.ReflectionFactory.newConstrutorForSerialization

如果你使用的应用程序是 Java 9 之前的版本,并且实现了任何 sun.*com.sun.* 包,那么在迁移到 Java 9 时你可能会遇到问题。为了解决这个问题,你应该检查你的类文件以确定是否使用了 sun.*com.sun.* 包。或者,你可以使用 Java 依赖分析工具 jdeps 来帮助确定你的 Java 程序是否有任何依赖 JDK 内部 API。

jdeps 工具是 Java 依赖分析工具,可以用来帮助确定你的 Java 程序是否有任何依赖 JDK 内部 API。

访问内部 JAR 文件

Java 9 不允许访问内部 JAR 文件,例如lib/ant-javax.jarlib/dt.jar以及在此处显示的lib目录中列出的其他文件:

这里要注意的关键点是,如果你有依赖于lib文件夹中这些工具之一的 Java 应用程序,你需要相应地修改你的代码。

建议你在开始使用 Java 9 之前测试你的 IDE,以确保 IDE 已更新并官方支持 Java 9。如果你使用多个 IDE 进行 Java 开发,测试每一个以避免意外。

JAR URL 弃用

在 Java 9 之前,JAR 文件 URL 被一些 API 用来标识运行时图像中的特定文件。这些 URL 包含一个jar:file:前缀和两个路径;一个指向jar,另一个指向jar内的特定资源文件。以下是 Java 9 之前 JAR URL 的语法:

    jar:file:<path-to-jar>!<path-to-file-in-jar>

随着 Java 9 模块系统的出现,容器将包含资源文件而不是单个 JAR 文件。访问资源文件的新语法如下:

    jrt:/<module-name>/<path-to-file-in-module>

现在有一个新的 URL 模式jrt用于在运行时图像内命名资源。这些资源包括类和模块。新的模式允许在不引入运行时图像安全风险的情况下识别资源。这种增强的安全性确保了运行时图像的形式和结构保持隐蔽。以下是新的模式:

    jrt:/[$MODULE[/$PATH]]

有趣的是,jrt URL 的结构决定了其含义,这表明结构可以采取几种不同的形式。以下是三个不同jrt URL 结构的示例:

  • jrt:/$MODULE/$PATH:这种结构提供了通过$PATH参数指定的资源文件,在由$MODULE参数指定的模块内的访问。

  • jrt:/$MODULE:这种结构提供了对由$MODULE参数指定的模块内所有资源文件的引用

  • jrt:/:这种结构提供了对运行时图像中所有资源文件的引用

如果你使用的代码已经包含了由 API 返回的 URL 实例,你应该不会遇到任何问题。另一方面,如果你的代码依赖于jar URL 结构,你将会遇到问题。

扩展机制

Java 平台之前有一个扩展机制,允许开发者将自定义 API 提供给所有应用程序。正如以下插图所示,扩展是某种插件,或者是 Java 平台的附加组件。每个扩展中的 API 和类默认情况下都是自动可用的:

如插图所示,Java 应用程序可以访问 Java 平台和扩展,而无需要求类路径。这个特性在 Java 8 中被弃用,并且在 Java 9 中不再存在。

JDK 的模块化

到目前为止,你已经对 Java 9 的模块化有了深刻的理解。Java 中的古老谚语,以及其他面向对象编程语言,是“万物皆类”。现在,随着 Java 9 的到来,“万物皆模块”成为新的谚语。有三种类型的模块,如下所述:

模块类型 描述
自动 当一个 JAR 文件放置在新的模块路径上时,模块会自动创建
显式/命名 这些模块通过编辑 module-info.java 文件手动定义
未命名 当一个 JAR 文件放置在类路径上时,会创建未命名的模块

当你将应用程序迁移到 Java 9 时,你的应用程序及其库将成为未命名的模块。因此,你需要确保所有模块都在模块路径中。

另一点需要注意是,你的运行时镜像将不会包含整个 JDK。相反,它将只包含你的应用程序所需的模块。值得回顾 Java 9 中 JDK 的模块化方式。以下表格包含 Java 9 中 JDK 的 API 规范:

jdk.accessibility jdk.attach jdk.charsets jdk.compiler
jdk.crypto.cryptoki jdk.crypto.ec jdk.dynalink jdk.editpad
jdk.hotspot.agent jdk.httpserver jdk.incubator.httpclient jdk.jartool
jdk.javadoc jdk.jcmd jdk.jconsole jdk.jdeps
jdk.jdi jdk.jdwp.agent jdk.jlink jdk.jshell
jdk.jsobject jdk.jstatd jdk.localedata jdk.management
jdk.management.agent jdk.naming.dns jdk.naming.rmi jdk.net
jdk.pack jdk.packager.services jdk.policytool jdk.rmic
jdk.scripting.nashorn jdk.sctp jdk.security.auth jdk.security.jgss
jdk.snmp jdk.xml.dom jdk.zipfs

以下表格包含 Java 9 中 Java SE 的 API 规范:

java.activation java.base java.compiler java.cobra
java.datatransfer java.desktop java.instrument java.logging
java.management java.management.rmi java.naming java.prefs
java.rmi java.scripting java.se java.se.ee
java.security.jgss java.security.sasi java.sql java.sql.rowset
java.transaction java.xml java.xml.bind java.xml.crypto
java.xml.ws java.xml.ws java.xml.ws.annotation

记住,所有应用程序都将默认访问 java.base,因为它在模块路径中。

以下表格包含 Java 9 中 JavaFX 的 API 规范:

javafx.base javafx.controls javafx.fxml javafx.graphics
javafx.media javafx.swing javafx.web

有两个额外的模块:

  • java.jnlp 定义了 JNLPJava 网络启动协议)的 API

  • java.smartcardio 定义了 Java Smart Card 输入/输出的 API

关于这些模块的详细信息,请访问 Oracle 的Java®平台,标准版和 Java 开发工具包版本 9 API 规范网站:download.java.net/java/jdk9/docs/api/overview-summary.html

来自 Oracle 的建议

Oracle 在将这个主要更新,版本 9,带到 Java 平台方面做得非常出色。他们对为 Java 9 做准备以及如何迁移到新 JDK 的见解值得回顾。在本节中,我们将探讨准备步骤、破坏封装、运行时图像的变化、已删除的工具和 API 组件、垃圾收集的变化以及部署。

准备步骤

Oracle 提供了一个五步流程来帮助开发者将他们的 Java 应用程序迁移到版本 9。这些步骤如下,并在随后的章节中进行详细说明:

  1. 获取 JDK 9 早期访问构建版本。

  2. 在重新编译前运行你的程序。

  3. 更新第三方库和工具。

  4. 编译你的应用程序。

  5. 在你的代码上运行jdeps

获取 JDK 9 早期访问构建版本

如果你在这本书正式发布 Java 9 之前阅读,你可以从这里获取 JDK 9 的早期访问构建版本——jdk.java.net/9/。早期发布构建版本适用于 Windows(32 位和 64 位)、macOS(64 位)、Linux(32 位和 64 位)以及各种 Linux ARM、Solaris 和 Alpine Linux 版本。

在 Java 9 正式发布之前,花时间测试你的应用程序以进行 Java 9 迁移,这有助于确保你的服务不会因为依赖于你的 Java 应用程序而出现停机。

在重新编译前运行程序

如本章前面所述,你的现有 Java 应用程序有可能在 Java 9 平台上无需修改即可运行。所以在你做出任何更改之前,尝试在 Java 9 平台上运行你的当前应用程序。如果你的应用程序在 Java 9 上运行良好,那很好,但你的工作还没有完成。请回顾下一节关于更新第三方库和工具、编译你的应用程序以及在代码上运行jdeps的内容。

更新第三方库和工具

第三方库和工具可以帮助我们扩展应用程序并缩短开发时间。为了与 Java 9 兼容,确保你使用的每个第三方库和工具都兼容并支持 JDK 的 9 版本是非常重要的。在 Java 9 上运行你的应用程序不会为你提供确保未来没有兼容性问题的洞察力。建议你查看每个库和工具的官方网站,以验证其与 JDK 9 的兼容性和支持情况。

如果你使用的库或工具有一个支持 JDK 9 的版本,请下载并安装它。如果你发现一个还不支持 JDK 9 的版本,考虑寻找替代品。

在我们的上下文中,工具包括 集成开发环境IDE)。NetBeans、Eclipse 和 IntelliJ 都有支持 JDK 9 的 IDE 版本。以下是如何访问这些网站的链接:

编译你的应用程序

你的下一步是使用 JDK 9 的 javac 编译你的应用程序。即使你的应用程序在 JDK 9 上运行良好,这也是很重要的。你可能不会收到编译器错误,但也要注意警告。以下是你可能不会使用 JDK 9 编译应用程序的最常见原因,假设它们在 Java 9 之前已经编译成功。

首先,如本章前面所述,大多数 JDK 9 内部 API 默认情况下不可访问。你的指示将是在运行时或编译时出现的 IllegalAccessErrors 错误。你需要更新你的代码,以便使用可访问的 API。

第二个可能导致你的 Java 9 之前的程序无法使用 JDK 9 编译的原因是如果你使用下划线字符作为单个字符标识符。根据 Oracle 的说法,这种做法在 Java 8 中会产生警告,在 Java 9 中会产生错误。让我们看一个例子。以下 Java 类实例化了一个名为 _ 的对象,并向控制台打印了一条单一的消息:

    public class Underscore 
    { 
      public static void main(String[] args) 
      {
        Object _ = new Object();
        System.out.println("This ran successfully.");
      }
    }

当我们使用 Java 8 编译此程序时,会收到一个警告,指出使用 '_' 作为标识符可能在 Java SE 8 之后的版本中不受支持:

如以下屏幕截图所示,这只是警告,应用程序运行正常:

现在,让我们尝试使用 JDK 9 编译相同的类:

如您所见,将下划线用作单个字符标识符仍然只会产生警告,而不会产生错误。应用程序运行成功。这次测试是在 JDK 9 仍处于早期发布阶段时运行的。假设在 JDK 9 正式发布后运行此测试将导致错误而不是仅仅警告。可能会抛出的错误如下:

Underscore.java:2: error: as of release 9, '_' is a keyword, and may not be used as a legal identifier.

即使这个问题在 JDK 9 的正式版本中未得到解决,将下划线用作单个字符标识符也不是好的编程实践,因此你应该避免使用它。

导致你的 Java 9 之前的程序无法使用 JDK 9 编译的第三个潜在原因是如果你使用了 -source-target 编译器选项。让我们看看 Java 9 之前和 Java 9 中的 -source-target 编译器选项。

Java 9 之前的 -source 和 -target 选项

-source 选项指定 Java SE 版本,并具有以下可接受值:

描述
1.3 javac 不支持 Java SE 1.3 之后引入的特性。
1.4 javac 接受 Java SE 1.4 中引入的语言特性。
1.5 或 5 javac 接受 Java SE 1.5 中引入的语言特性。
1.6 或 6 javac 将编码错误报告为错误而不是警告。值得注意的是,Java SE 1.6 没有引入新的语言特性。
1.7 或 7 javac 接受 Java SE 1.7 中引入的语言特性。如果未使用 -source 选项,这是默认值。

-target 选项告诉 javac 要针对哪个 JVM 版本。-target 选项的可接受值是--1.11.21.31.41.551.661.77。如果没有使用 -target 选项,默认 JVM 目标取决于与 -source 选项一起使用的值。以下是 -source 值及其相关 -target 的表格:

-source 值 默认 -target
未指定 1.7
1.2 1.4
1.3 1.4
1.4 1.4
1.5 或 5 1.7
1.6 或 6 1.7
1.7 1.7

Java 9 -source 和 -target 选项

在 Java 9 中,支持值如下所示:

支持值 备注
9 这是默认值,如果没有指定任何值
8 将支持设置为 1.8
7 将支持设置为 1.7
6 将支持设置为 1.6 并生成一个警告(不是错误)以指示 JDK 6 已过时

在您的代码上运行 jdeps

jdeps 类依赖分析工具对 Java 9 来说并不陌生,但随着 Java 9 的到来,它可能对开发者来说从未如此重要。将您的应用程序迁移到 Java 9 的重要一步是运行 jdeps 工具,以确定您的应用程序及其库的依赖项。如果您的代码依赖于任何内部 API,jdeps 工具会很好地建议替代方案。

以下截图显示了使用 jdeps 分析器时您可用的选项:

让我们看看一个例子。以下是一个简单的名为 DependencyTest 的 Java 类:

    import sun.misc.BASE64Encoder;

    public class DependencyTest 
    {
      public static void main(String[] args) throws
       InstantiationException, IllegalAccessException 
      {
        BASE64Encoder.class.newInstance();
        System.out.println("This Java app ran successfully.");
      }
    }

现在,让我们使用 javac 使用 Java 8 编译这个类:

如您所见,Java 8 成功编译了这个类,应用程序也运行了。编译器确实给出了一个 DependencyTest.java:6: warning: BASE64Encoder 是内部专有 API,可能在未来的版本中删除 警告。现在,让我们看看当我们尝试使用 Java 9 编译这个类时会发生什么:

在这种情况下,使用 Java 9,编译器给出了两个警告而不是一个。第一个警告是针对import sun.misc.BASE64Encoder;语句的,第二个是针对BASE64Encoder.class.newInstance();方法调用的。如您所见,这些只是警告而不是错误,因此DependencyTest.java类文件成功编译。接下来,让我们运行应用程序:

图片

现在,我们可以清楚地看到 Java 9 不会允许我们运行应用程序。接下来,让我们使用jdeps分析工具运行一个依赖性测试。我们将使用以下命令行语法--jdeps DependencyTest.class:

图片

如您所见,我们有三个依赖项:java.iojava.langsun.misc。在这里,我们得到了将我们的sun.misc依赖项替换为rt.jar的建议。

破坏封装

由于 Java 9 平台在模块重组后增加了封装,因此与前辈版本相比,Java 9 平台更加安全。话虽如此,您可能需要突破模块系统的封装。Java 9 允许突破这些访问控制边界。

正如您在本章中之前所读到的,大多数内部 API 都进行了强封装。正如之前所建议的,在更新源代码时,您可能会寻找替代 API。当然,这并不总是可行的。您还可以采取三种额外的方法--在运行时使用--add-opens选项;使用--add-exports选项;以及--permit-illegal-access命令行选项。让我们看看这些选项中的每一个。

--add-opens 选项

您可以使用--add-opens运行时选项来允许您的代码访问非公共成员。这可以被称为深度反射。执行深度反射的库能够访问所有成员,包括私有和公共成员。为了授予这种类型的访问权限给您的代码,您使用--add-opens选项。以下是语法:

    --add-opens module/package=target-module(,target-module)*

这允许给定的模块打开指定的包。当使用此功能时,编译器不会产生任何错误或警告。

--add-exports 选项

您可以使用--add-exports来破坏封装,以便您可以使用默认情况下不可访问的内部 API。以下是语法:

    --add-exports <source-module>/<package>=<target-module>(
     ,<target-module>)*

此命令行选项允许<target-module>中的代码访问<source-module>包中的类型。

破坏封装的另一种方法是使用 JAR 文件的清单。以下是一个示例:

    --add-exports:java.management/sun.management

应仅在绝对必要时使用--add-exports命令行选项。不建议除了短期解决方案之外使用此选项。常规使用此选项的危险在于,任何对引用的内部 API 的更新都可能导致您的代码无法正常工作。

--permit-illegal-access 选项

打破封装的第三种方法是使用--permit-illegal-access选项。当然,与第三方库创建者联系以查看他们是否有更新的版本是明智的。如果没有这个选项,你可以使用--permit-illegal-access来获取对类路径上要实现的操作的非法访问。由于这里的操作非法性显著,每次这些操作发生时,你都将收到警告。

运行时镜像变化

Java 9 对 JDK 和 JRE 进行了重大改变。其中许多变化与模块化相关,已在其他章节中介绍。还有一些其他事项你应该考虑。

Java 版本架构

在 Java 9 中,Java 平台版本显示的方式已改变。以下是一个 Java 9 之前版本格式的示例:

图片

现在,让我们看看 Java 9 如何报告其版本:

图片

如你所见,Java 9 的版本架构现在是$MAJOR.$MINOR.$SECURITY.$PATCH。这与 Java 的先前版本有显著不同。这只会影响你的应用程序,如果你有解析java -version命令返回的字符串的代码。

JDK 和 JRE 布局

在 Java 新版本中,JDK 和 JRE 的文件组织方式已发生变化。熟悉新的文件系统布局是值得你花时间的。以下截图显示了 JDK 的/bin文件夹的文件结构:

图片

下面是\lib文件夹的布局:

图片

已移除的内容

Java 平台新版本的变化还包括许多平台组件已被移除。以下部分代表了最显著的组件。

值得注意的是,rt.jartools.jardt.jar已被移除。这些 JAR 文件包含类和其他资源文件,并且都位于/lib目录中。

授权标准覆盖机制已被移除。在 Java 9 中,如果javacjava检测到该机制,它们将退出。该机制被用于应用程序服务器以覆盖一些 JDK 组件。在 Java 9 中,你可以使用可升级的模块来实现相同的结果。

如本章之前所述,扩展机制也已移除。

以下列出的 API 之前已被弃用,已被移除,且在 Java 9 中不可访问。这些 API 的移除是 Java 平台模块化的结果:

  • apple.applescript

  • com.apple.concurrent

  • com.sun.image.codec.jpeg

  • java.awt.dnd.peer

  • java.awt.peer

  • java.rmi.server.disableHttp

  • java.util.logging.LogManager.addPropertyChangeListener

  • java.util.logging.LogManager.removePropertyChangeListener

  • java.util.jar.Pack200.Packer.addPropertyChangeListener

  • java.util.jar.Pack200.Packer.removePropertyChangeListener

  • java.util.jar.Pack200.Unpacker.addPropertyChangeListener

  • java.util.jar.Pack200.Unpacker.removePropertyChangeListener

  • javax.management.remote.rmi.RMIIIOPServerImpl

  • sun.misc.BASE64Encoder

  • sun.misc.BASE64Decoder

  • `sun.rmi.transport.proxy.connectTimeout`

  • sun.rmi.transport.proxy.eagerHttpFallback

  • sun.rmi.transport.proxy.logLevel

  • sun.rmi.transport.tcp.proxy

以下列出的工具已被移除。在每种情况下,该工具之前已被淘汰或其功能已被更好的替代方案所取代:

  • hprof

  • java-rmi.cgi

  • java-rmi.exe

  • JavaDB

  • jhat

  • native2ascii

Java 9 中删除的另外两项是:

  • AppleScript 引擎。该引擎被认为不可用,并且没有替代品而被删除。

  • Windows 32 位客户端虚拟机。JDK 9 支持 32 位服务器 JVM,但不支持 32 位客户端 VM。这一变化是为了专注于 64 位系统的性能提升。

更新的垃圾回收

垃圾回收一直是 Java 的亮点之一。在 Java 9 中,垃圾-首次G1)垃圾回收器现在是 32 位和 64 位服务器上的默认垃圾回收器。在 Java 8 中,默认的垃圾回收器是并行垃圾回收器。Oracle 报告称,有三种垃圾回收组合将阻止你的应用程序在 Java 9 中启动。这些组合是:

  • DefNew + CMS

  • 增量 CMS

  • ParNew + SerialOld

我们将在第七章利用新的默认 G1 垃圾回收器中深入探讨 Java 9 的垃圾回收。

部署

在将应用程序迁移到 Java 9 的过程中,你应该注意三个问题。这些问题是 JRE 版本选择、序列化小程序和 JNLP 的更新。

JNLPJava 网络启动协议的缩写,将在本章的后续部分中介绍。

JRE 版本选择

在 Java 9 之前,开发人员可以在启动应用程序时请求除启动版本之外的 JRE 版本。这可以通过命令行选项或适当的 JAR 文件清单配置来实现。由于我们通常以这种方式部署应用程序,此功能已在 JDK 9 中被移除。以下是三种主要方法:

  • 激活安装程序

  • Java Web Start使用 JNLP

  • 原生操作系统打包系统

序列化小程序

Java 9 不支持将小程序作为序列化对象部署的能力。在过去,小程序作为序列化对象部署以补偿缓慢的压缩和 JVM 性能问题。随着 Java 9 的推出,压缩技术得到了提升,JVM 的性能也非常出色。

如果你尝试将小程序作为序列化对象部署,当小程序启动时,你的对象属性和参数标签将被简单地忽略。从 Java 9 开始,你可以使用标准部署策略部署小程序。

JNLP 更新

JNLP 用于在桌面客户端上使用位于 Web 服务器上的资源启动应用程序。JNLP 客户端包括 Java Web Start 和 Java Plug-in 软件,因为它们能够启动远程托管的小程序。此协议对于启动 RIAs 至关重要。

RIAs(富互联网应用)在通过 JNLP 启动时,可以访问各种 JNLP API,在用户授权的情况下,可以访问用户的桌面。

在 Java 9 中,JNLP 规范已更新。以下几节详细介绍了四个具体的更新。

嵌套资源

在 Java 或 j2se 元素中使用具有嵌套资源的组件扩展以前是支持的,但在规范中并未记录。规范现已更新以反映这一支持。以前的规范读作:

不能将任何 java 元素指定为资源的一部分。

Java 9 的更新规范现在读作:

组件扩展中的 java 元素不会控制使用哪个版本的 Java,但它可以包含嵌套的资源元素,然后只有在使用与第 4.6 节中指定的版本匹配的 Java 版本时,才能使用这些资源。

此特定更改确保扩展 JLP 文件必须具有javaj2se资源,并且这些资源不会决定使用哪个 JRE。当使用指定的版本时,允许嵌套资源。

FX XML 扩展

当使用 JNLP 时,你会创建一个 JNLP 文件。以下是一个示例:

    <?xml version="1.0" encoding="UTF-8"?>
    <jnlp spec="1.0+" codebase="" href="">
      <information>
        <title>Sample/title>
        <vendor>The Sample Vendor</vendor>
        <icon href="sample-icon.jpg"/>
        <offline-allowed/>
     </information>
     <resources>
       <!-- Application Resources -->
       <j2se version="1.6+"  
        href="http://java.sun.com/products/autodl/j2se"/>
       <jar href="Sample-Set.jar" main="true" />
     </resources>
     <application-desc
       name="Sample Application"
       main-class="com.vendor.SampleApplication" 
       width="800" 
       height="500">
       <argument>Arg1</argument>
       <argument>Arg2</argument>
       <argument>Arg3</argument>
     </application-desc>
     <update check="background"/>
    </jnlp>

<application-desc>元素进行了两项更改。首先,添加了可选的type属性,以便可以注释应用程序的类型。默认类型是Java,因此如果你的程序是 Java 应用程序,你不需要包含type属性。或者,你可以如下指定Java作为你的类型:

    <application-desc 
      name="Another Sample Application"
      type="Java" main-class="com.vendor.SampleApplication2" 
      width="800" 
      height="500">
      <argument>Arg1</argument>
      <argument>Arg2</argument>
      <argument>Arg3</argument>
    </application-desc>

我们可以指示其他应用程序类型,例如在此处所示包含JavaFX

    <application-desc 
      name="A Great JavaFX Application"
      type="JavaFX" main-class="com.vendor.GreatJavaFXApplication" 
      width="800" 
      height="500">
      <argument>Arg1</argument>
      <argument>Arg2</argument>
      <argument>Arg3</argument>
    </application-desc>

如果你指定了一个 JNLP 客户端不支持的应用程序类型,你的应用程序启动将失败。有关 JNLP 的更多信息,你可以查阅官方文档:docs.oracle.com/javase/7/docs/technotes/guides/javaws/developersguide/faq.html

Java 9 中对<application-desc>元素的第二次更改是添加了param子元素。这允许我们使用value属性提供参数的名称及其值。以下是一个示例,展示了包含param子元素和value属性的<application-desc>元素的外观。此示例显示了三组参数:

    <application-desc
      name="My JRuby Application"
      type="JRuby"
      main-class="com.vendor.JRubyApplication" 
      width="800" 
      height="500">
      <argument>Arg1</argument>
      <argument>Arg2</argument>
      <argument>Arg3</argument>
      <param name="Parameter1" value="Value1"/>
      <param name="Parameter2" value="Value2"/>
      <param name="Parameter3" value="Value3"/>
    </application-desc>

如果应用程序type是 Java,那么你使用的任何param子元素都将被忽略。

JNLP 文件语法

JNLP 文件语法现在完全符合 XML 规范。在 Java 9 之前,你可以使用 & 来创建复杂的比较。这不符合标准 XML。你仍然可以在 JNLP 文件中创建复杂的比较。现在你将使用 &amp; 而不是 &

数字版本比较

JNLP 规范已更改,以反映数字版本元素与非数字版本元素的比较方式。在更改之前,版本元素是按 ASCII 值的字典顺序比较的。在 Java 9 和这次 JNLP 规范更改之后,元素仍然按 ASCII 值的字典顺序比较。当两个字符串长度不同时,这种变化是明显的。在新比较中,较短的字符串将用前导零填充,以匹配较长的字符串长度。

字典序比较使用基于字母顺序的数学模型。

有用的工具

在将你的应用程序迁移到 Java 9 之前,你需要首先下载 JDK 9。你可以从这个网址下载早期访问版本--jdk.java.net/9/。你需要接受许可协议,然后选择要下载的版本。正如你在下面的截图中所看到的,根据你的操作系统,有几个选项:

现在你已经在你的开发计算机上安装了 JDK 9,让我们看看一些可以帮助你将应用程序迁移到 Java 9 的工具。

Java 环境 - jEnv

如果你在一台装有 Linux 或 macOS 的计算机上开发,你可能考虑使用 jEnv,这是一个开源的 Java 环境管理工具。这是一个命令行工具,所以不要期待有 GUI。你可以通过这个网址下载工具--github.com/gcuisinier/jenv

这里是 Linux 的安装命令:

$ git clone https://github.com/gcuisinier/jenv.git ~/.jenv

要使用 macOS 和 Homebrew 下载,使用以下命令:

$ brew install jenv

你也可以使用 Bash 在 Linux 或 macOS 上进行安装,如下所示:

$ echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.bash_profile
$ echo 'eval "$(jenv init -)"' >> ~/.bash_profile

或者,你可以使用 Zsh 在 Linux 或 macOS 上进行安装,如下所示:

$ echo 'export PATH="$HOME/.jenv/bin:$PATH"' >> ~/.zshrc
$ echo 'eval "$(jenv init -)"' >> ~/.zshrc

在安装 jEnv 之后,你需要按照以下方式在你的系统上配置它。你需要修改脚本以反映你的实际路径:

$ jenv add /Library/Java/JavaVirtualMachines/jdk17011.jdk/Contents/Home

你需要为系统上每个 JDK 版本重复执行 jenv add 命令。每次执行 jenv add 命令时,你将收到确认,表明特定的 JDK 版本已被添加到 jEnv,如下所示:

$ jenv add /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
  oracle64-1.6.0.39 added
$ jenv add /Library/Java/JavaVirtualMachines/jdk17011.jdk/Contents/Home
  oracle64-1.7.0.11 added

你可以通过在命令提示符中使用 $ jenv versions 来检查你添加到 jEnv 的 JDK 版本。这将产生一个输出列表。

这里是三个额外的 jEnv 命令:

  • jenv global <version>:这设置了全局版本

  • jenv local <version>:这设置了本地版本

  • jenv shell <version>:这设置了 shell 的实例版本

Maven

Maven 是一个开源工具,可用于构建和管理基于 Java 的项目。它已经支持 Java 9,并作为Apache Maven 项目的一部分。如果您尚未使用 Maven,并且您进行大量的 Java 开发,您可能会被以下 Maven 目标所吸引:

  • 使构建过程简单化

  • 提供统一的构建系统

  • 提供高质量的项目信息

  • 提供最佳实践开发的指南

  • 允许透明迁移到新功能

您可以在该网站上阅读有关每个 Maven 目标的详细信息--maven.apache.org/what-is-maven.html。要下载 Maven,请访问此网站--maven.apache.org/download.cgi。Windows、macOS、Linux 和 Solaris 的安装说明可在maven.apache.org/install.html找到。

Maven 可以与 Eclipse(M2Eclipse)、JetBrains IntelliJ IDEA 和 Netbeans IDE 集成。以 M2Eclipse IDE 为例,它提供了丰富的 Apache Maven 集成,并具有以下功能:

  • 您可以在 Eclipse 中启动 Maven 构建

  • 管理 Eclipse 构建路径的依赖项

  • 轻松解决 Maven 依赖项(您可以直接从 Eclipse 操作,而无需安装本地 Maven 仓库)

  • 自动下载所需的依赖项(来自远程 Maven 仓库)

  • 使用软件向导创建新的 Maven 项目,创建pom.xml文件,并启用对您的纯 Java 项目的 Maven 支持

  • 快速搜索 Maven 远程仓库的依赖项

获取 M2Eclipse IDE

要获取 M2Eclipse IDE,您必须首先安装 Eclipse。以下是步骤:

  1. 首先打开您的当前 Eclipse IDE。接下来,选择首选项 | 安装/更新 | 可用软件站点,如以下截图所示:

图片

  1. 下一个任务是向您的可用软件站点列表中添加 M2Eclipse 仓库站点。为此,点击添加按钮,并在名称和位置文本输入框中输入值。对于名称,输入一些可以帮助您记住 M2Eclipse 位于此站点的信息。对于位置,输入 URL--http://download.eclipse.org/technology/m2e/releases。然后,点击确定按钮:

图片

  1. 现在,您应该会在可用软件站点列表中看到 M2Eclipse 站点,如以下截图所示。您的最后一步是点击确定按钮:

图片

  1. 现在,当您开始一个新项目时,您将看到Maven Project作为选项:

图片

Maven 是 Java 开发者证明的工具。您可以考虑通过以下资源之一获取有关 Maven 的更多信息:

摘要

在本章中,我们探讨了将现有应用程序迁移到 Java 9 平台可能涉及的问题。我们考虑了手动和半自动迁移过程。本章为您提供了使您的 Java 8 代码与 Java 9 兼容的见解和流程。具体来说,我们快速回顾了 Project Jigsaw,探讨了模块如何融入 Java 生态系统,提供了迁移规划的建议,分享了 Oracle 关于迁移的建议,并分享了您在开始使用 Java 9 时可以使用的工具。

在下一章中,我们将仔细研究 Java shell 和 JShell API。我们将展示 JShell API 和 JShell 工具交互式评估 Java 编程语言的声明、语句和表达式的功能。我们将演示这个命令行工具的功能和使用方法。

第六章:尝试 Java Shell

在上一章中,我们探讨了如何将 Java 9 之前的应用程序迁移到新的 Java 平台。我们检查了可能导致您当前应用程序在 Java 9 上运行出现问题的几个问题。我们从对 Project Jigsaw 的回顾开始,然后探讨了模块如何适应新的 Java 平台。我们为您提供了使 Java 8 代码与 Java 9 一起工作的见解和流程。具体来说,我们提供了迁移规划的建议,分享了 Oracle 关于迁移的建议,并分享了您在开始使用 Java 9 时可以使用的工具。

在本章中,我们将首次了解 Java 9 中的新命令行工具,读取-评估-打印循环(也称为 REPL),即 Java ShellJShell)。我们将从有关此工具、读取-评估-打印循环概念的基本信息开始,然后进入用于 JShell 的命令和命令行选项。我们将采用实践者的方法来审查 Java Shell,并包括您可以自己尝试的示例。

本章涵盖了以下主题:

  • 什么是 JShell?

  • 开始使用 JShell

  • JShell 的实际用途

  • 与脚本一起工作

什么是 JShell?

JShell 是 Java 9 中引入的新工具。它是一个交互式读取-评估-打印循环工具,用于评估以下 Java 编程语言组件——声明、语句和表达式。它有自己的 API,因此可以由外部应用程序使用。

读取-评估-打印循环通常被称为 REPL,取自短语中每个单词的首字母。它也被称为语言外壳或交互式顶层。

JShell 的引入是 Java Enhancement ProgramJEP)222 的结果。以下是关于 Java Shell 命令行工具的 JEP 的声明目标:

  • 促进快速调查

  • 促进快速编码

  • 提供编辑历史记录

之前列出的快速调查和编码包括语句和表达式。令人印象深刻的是,这些语句和表达式不需要是方法的一部分。此外,变量和方法也不需要是类的一部分,这使得此工具特别灵活。

此外,以下列出的功能被包括进来,使 JShell 更易于使用,并使您使用 JShell 的时间尽可能高效:

  • Tab-completion

  • 语句末尾分号的自动完成

  • 导入自动完成

  • 定义自动完成

开始使用 JShell

JShell 是一个位于 /bin 文件夹中的命令行工具。此工具的语法为 jshell <options> <load files>。正如您所看到的,此工具有多个可用的选项:

图片

您已经看到了 -h 选项,我们通过 jshell -h 执行了它。这提供了 JShell 选项的列表。

要登录 JShell,你只需使用jshell命令。你会看到命令窗口中的提示符相应地改变:

图片

退出 shell 就像输入/exit一样简单。一旦进入 JShell,你可以输入以下任何命令:

命令 功能

| /drop | 使用此命令通过nameid引用删除源条目。以下是语法:

    /drop <name or id>

|

| /edit | 使用此命令,你可以使用nameid引用来编辑源条目。以下是语法:

    /edit <name or id>

|

| /env | 此强大命令允许你查看或更改评估上下文。以下是语法:

    /env [-class-path <path>] [-module-path <path>]
     [-add-modules <modules]

|

/exit 此命令用于退出 JShell。语法很简单,只需输入/exit,没有可选的选项或参数。
/history 历史命令提供了你输入的历史记录。语法很简单,只需输入/history,没有可选的选项或参数。
/<id> 此命令通过引用id重新运行之前的代码片段。以下是语法:/<id>你也可以通过引用第n个之前的代码片段来运行特定的代码片段,使用/-<n>
/imports 你可以使用此命令列出导入的项目。语法是/imports,它不接受任何选项或参数。

| /list | 此命令将列出你输入的源代码。以下是语法:

    /list [<name or id> &#124; -all &#124; -start]

|

| /methods | 此命令列出所有声明的方法以及它们的签名。以下是语法:

    /methods [<name or id> &#124; -all &#124; -start]

|

| /open | 使用此命令,你可以将文件作为源输入打开。以下是语法:

    /open <file>

|

| /reload | 重新加载命令允许你重置并重新播放相关历史记录。以下是语法:

    /reload [-restore] [-quiet]  [-class-path 
     <path>] [-module-path <path>]

|

| /reset | 此命令重置 JShell。以下是语法:

    /reset [-class-path <path>] [-module-path 
     <path>] [-add-modules <modules]

|

| /save | 此命令将代码片段源保存到你指定的文件中。以下是语法:

    /save [-all &#124; -history &#124; -start] <file>

|

| /set | 此命令用于设置 JShell 的配置信息。以下是语法:

    /set editor &#124; start &#124; feedback &#124; mode &#124; prompt &#124; 
     truncation &#124; format

|

| /types | 此命令简单地列出声明的类型。以下是语法:

    /types [<name or id> &#124; -all &#124; -start]

|

| /vars | 此命令列出所有声明的变量以及它们的值。以下是语法:

    /vars [<name or id> &#124; -all &#124; -start]

|

/! 此命令将重新运行最后一个代码片段。语法很简单,只需输入/!

列出的几个命令使用了术语代码片段。在 Java 9 和 JShell 的上下文中,代码片段是以下之一:

  • 类声明

  • 表达式

  • 字段声明

  • 导入声明

  • 接口声明

  • 方法声明

在 JShell 中输入/help/?命令将提供可以在 shell 中使用的完整命令和语法列表。该列表如下所示:

图片

/help命令对于初学者特别有帮助。正如以下截图所示,我们可以通过简单地输入/help intro命令来获取 JShell 的介绍:

图片

如果你经常使用 JShell,你可能从以下列出的一个或多个快捷键中受益。你可以在 JShell 内部使用/help shortcuts命令在任何时候列出这些快捷键:

图片

你可以通过在 JShell 中使用/help命令后跟你想获取更多帮助的命令来从 JShell 内部获得额外的帮助。例如,输入/help reload提供了关于/reload命令的详细信息。该信息如下:

图片

JShell 的实际用途

无论你是新开发者、经验丰富的开发者,还是刚刚接触 Java,你都会发现 JShell 非常有用。在本节中,我们将探讨 JShell 的一些实际用途。具体来说,我们将涵盖:

  • 反馈模式

  • 列出你的资产

  • 在 JShell 中编辑

反馈模式

命令行工具通常为了不使屏幕过于拥挤或成为开发者的麻烦,会提供相对稀疏的反馈。JShell 除了给开发者提供创建自定义模式的能力外,还有几种反馈模式。

如以下截图所示,有四种反馈模式--concisenormalsilentverbose。在这里,我们没有提供任何参数就输入了/set feedback命令,以列出反馈模式以及确定当前的反馈模式。输出第一行显示了将模式设置为当前设置模式的命令行命令和参数集。因此,在以下截图中,当前的反馈模式设置为verbose,其他三种模式被列出:

图片

我们可以在启动 JShell 时包含一个选项来指定我们第一次进入 JShell 时想要进入的模式。以下是命令行选项:

命令行命令和选项 反馈模式
jshell -q concise
jshell -n normal
jshell -s silent
jshell -v verbose

你会注意到我们使用-q来表示concise模式,而不是-c-c选项具有-c<flag>语法,用于将<flag>传递给编译器。

最好的方法是使用示例来回顾反馈模式之间的差异。从normal模式开始,我们将执行命令行命令来完成以下顺序的反馈演示:

  1. 创建一个变量。

  2. 更新变量的值。

  3. 创建一个方法。

  4. 更新该方法。

  5. 运行该方法。

要开始我们的第一次测试,我们将在jshell>提示符下执行/set feedback normal命令,这将把 JShell 的反馈模式设置为normal。进入normal反馈模式后,我们将输入必要的命令来运行我们的演示:

图片

在进入normal反馈模式后,我们输入了int myVar = 3并收到了myVar ==> 3作为反馈。在我们的下一个命令中,我们更改了相同变量的值,并收到了相同的新值输出。我们的下一个语句void quickMath() {System.out.println("Your result is " + (x*30 + 19));}使用了未声明的变量,您可以看到结果是两部分的反馈——一部分指示方法已创建,另一部分通知该方法在未声明变量声明之前不能被调用。接下来,我们将方法更改为包含myVar变量,反馈报告了方法已被修改。我们的最后一步是使用quickMath();运行该方法,结果正如我们所预期的。

让我们在concise模式下尝试相同的反馈演示:

图片

如您从前面的截图中所见,concise反馈模式为我们提供了更少的反馈。我们创建和修改了变量,但没有收到任何反馈。当我们创建具有未声明变量的方法时,我们收到了与normal模式相同的反馈。我们更新了方法而没有确认或其他反馈。

我们下一次使用反馈演示将在silent模式下:

图片

当我们进入silent反馈模式时,正如您可以从前面的截图看到的那样,JShell 提示符从jshell>变为->。当我们创建myVar变量、修改myVar变量或创建quickMath()方法时,没有提供任何反馈。我们故意创建quickMath()方法来使用未声明的变量。因为我们处于silent反馈模式,所以我们没有得到方法有未声明变量的通知。基于这种缺乏反馈的情况,我们运行了该方法,但没有提供任何输出或反馈。接下来,我们更新了方法以包含已声明的myVar变量,然后再次运行了该方法。

silent反馈模式可能看起来没有意义,因为没有提供任何反馈,但这个模式有很大的实用性。使用silent模式可能适合管道线或简单地当您想最小化终端输出量时。例如,您可以使用隐式的System.out.println命令包含特定的、条件性的输出。

我们上一次使用反馈演示是在verbose反馈模式下。这种反馈模式,正如其名称所暗示的,提供了最多的反馈。以下是我们的测试结果:

图片

在我们的反馈演示中,使用verbose反馈模式,我们收到了更多反馈以及更美观的反馈格式。

创建自定义反馈模式

虽然内部反馈模式(normalconcisesilentverbose)不能修改,但您可以创建自己的自定义反馈模式。这个过程的第一步是复制一个现有模式。以下示例演示了如何使用/set mode myCustom verbose -command命令字符串将verbose模式复制到myCustom模式:

我们使用-command选项以确保我们会收到命令反馈。您可以使用/set命令以及以下屏幕截图中所列出的选项之一来更改您的反馈模式:

例如,让我们通过截断设置来了解它强制指定每行输出显示多少个字符。使用/set truncation命令,如以下屏幕截图所示,显示了当前的截断设置:

如您所见,我们的myCustom反馈模式的截断为80。我们将使用/set truncation myCustom 60命令将其更改为60,然后使用/set truncation命令进行验证:

如您在前一个屏幕截图中所见,我们的myCustom反馈模式的截断已成功从继承自verbose模式的80更改为60,这是基于我们使用/set truncation myCustom 60 JShell 命令的结果。

列出资产

有几个 JShell 命令方便列出您创建的资产。使用上一节中的反馈演示,我们执行了/vars, /methods/list命令,分别提供变量、方法和所有源列表:

我们还可以使用/list -all命令和选项组合来查看 JShell 导入了哪些包。如以下屏幕截图所示,JShell 导入了几个使我们在 shell 中的工作更方便的包,节省了我们不得不在我们的方法中导入这些标准包的时间:

如果您只想列出启动导入,可以使用/list -start命令和选项组合。如以下屏幕截图所示,每个启动导入都有一个"s"前缀,并且是按数字顺序排列的:

在 JShell 中编辑

JShell 不是一个功能齐全的文本编辑器,但在 shell 中您可以做几件事情。本节为您提供了编辑技巧,分为修改文本、基本导航、历史导航和高级编辑命令。

修改文本

默认的文本编辑/输入模式是您输入的文本将出现在当前光标位置。当您想要删除文本时,您有几种选项可供选择。以下是一个完整的列表:

删除操作 PC 键盘组合 Mac 键盘组合
删除当前光标位置的字符 Delete Delete
删除光标左侧的字符 Backspace Backspace
从光标位置删除到行尾 Ctrl + K Cmd + K
从光标位置删除到当前单词的末尾 Alt + D Alt/Opt + D
从光标位置删除到前一个空白字符 Ctrl + W Cmd + W
在光标位置粘贴最近删除的文本 Ctrl + Y Cmd + Y
当使用 Ctrl + Y(或 Mac 上的 Cmd + Y)时,您将能够使用 Alt + Y 键盘组合来循环浏览之前删除的文本 Alt + Y Alt/Opt + Y

基本导航

虽然在 JShell 内部的导航控制与大多数命令行编辑器相似,但有一个基本导航控制列表是有帮助的:

键/键组合 导航操作
左箭头 向后移动一个字符
右箭头 向前移动一个字符
上箭头 向后移动一行,通过历史记录
下箭头 向前移动一行,通过历史记录
Return 输入(提交)当前行
Ctrl + A (Mac 上的 cmd - A) 跳转到当前行的开头
Ctrl + E (Mac 上的 cmd - E) 跳转到当前行的末尾
Alt + B 向后跳转一个单词
Alt + F 向前跳转一个单词

历史导航

JShell 记录您输入的片段和命令。它维护这个历史记录,以便您可以重用已经输入的片段和命令。要循环浏览片段和命令,您可以按住 Ctrl 键(Mac 上的 cmd),然后使用上下箭头键,直到看到您想要的片段或命令。

高级编辑命令

还有更多编辑选项,包括搜索功能、宏的创建和使用等。JShell 的编辑器基于 JLine2,这是一个用于解析控制台输入和编辑的 Java 库。您可以在以下网址了解更多关于 JLine2 的信息:github.com/jline/jline2/wiki/JLine-2.x-Wiki

处理脚本

到目前为止,您已经直接从键盘输入数据到 JShell。您还可以处理 JShell 脚本,这些脚本是一系列 JShell 命令和片段。格式与其他脚本格式相同,每行一个命令。

在本节中,我们将查看启动脚本,检查如何加载脚本,如何保存脚本,然后以使用 JShell 的高级脚本结束。

启动脚本

每次启动 JShell 时,都会加载启动脚本。这也发生在使用 /reset/reload/env 命令时。

默认情况下,JShell 使用 DEFAULT 启动脚本。如果你想使用不同的启动脚本,只需使用 /set start <script> 命令。以下是一个示例--/set start MyStartupScript.jsh。或者,你可以在命令提示符中使用 jshell --start MyStartupScript.jsh 命令来启动 JShell 并加载 MyStartupScript.jsh JShell 启动脚本。

当你使用带有 -retain 选项的 /set start <script> 命令时,你是在告诉 JShell 在你下次启动 JShell 时使用新的启动脚本。

加载脚本

在 JShell 中加载脚本可以通过以下方法之一完成:

  • 你可以使用 /open 命令,并带上脚本名称作为参数。例如,如果我们的脚本名称是 MyScript,我们会使用 /open MyScript

  • 加载脚本的第二种方法是使用命令提示符中的 jshell MyScript.jsh。这将启动 JShell 并加载 MyScript.jsh JShell 脚本。

保存脚本

除了在外部编辑器中创建 JShell 脚本外,我们还可以在 JShell 环境中创建它们。采用这种方法时,你需要使用 /save 命令来保存你的脚本。正如以下截图所示,/save 命令至少需要一个文件名参数:

图片

/save 命令有三个可用的选项:

  • -all 选项可以用来将所有代码片段的源代码保存到指定的文件中。

  • -history 选项保存自 JShell 启动以来输入的所有命令和代码片段的顺序历史。JShell 能够执行此操作,表明它维护着你输入的所有内容的记录。

  • -start 选项将当前启动定义保存到指定的文件中。

使用 JShell 进行高级脚本编写

JShell 的限制是什么?你可以用这个工具做很多事情,而你实际上只受限于你的想象力和编程能力。

让我们看看一个可以用来从 JShell 脚本编译和运行 Java 程序的高级代码库:

    import java.util.concurrent.*
    import java.util.concurrent.*
    import java.util.stream.*
    import java.util.*

    void print2Console(String thetext) 
    { 
      System.out.println(thetext); 
      System.out.println("");
    }

    void runSomeProcess(String... args) throws Exception 
    { 
      String theProcess = 
        Arrays.asList(args).stream().collect(
         Collectors.joining(" ")); 
        print2Console("You asked me to run: '"+theProcess+"'"); 
        print2Console(""); 
        ProcessBuilder compileBuilder = new 
          ProcessBuilder(args).inheritIO(); 
        Process compileProc = compileBuilder.start(); 
        CompletableFuture<Process> compileTask =
         compileProc.onExit(); 
        compileTask.get();
    }

    print2Console("JShell session launched.")
    print2Console("Preparing to compile Sample.java. . . ")

    // run the Java Compiler to complete Sample.java
    runSomeProcess("javac", "Sample.java")
    print2Console("Compilation complete.")
    print2Console("Preparing to run Sample.class...")

    // run the Sample.class file
    runSomeProcess("java", "Sample")
    print2Console("Run Cycle compete.")

    // exit JShell
    print2Console("JShell Termination in progress...)
    print2Console("Session ended.")

    /exit

如此脚本所示,我们创建了一个 runSomeProcess() 方法,并可以使用它显式地编译和运行外部 Java 文件。

概述

在本章中,我们探讨了 JShell,Java 9 的新读-求值-打印循环命令行工具。我们从关于该工具的简介开始,并仔细研究了读-求值-打印循环的概念。我们花了大量时间回顾 JShell 命令和命令行选项。我们的内容涵盖了反馈模式、资产列表和壳中编辑的实际指南。我们还获得了使用脚本的实践经验。

在下一章中,我们将探讨 Java 9 的新默认垃圾回收器。具体来说,我们将探讨默认垃圾回收、已弃用的垃圾回收组合,并检查垃圾回收日志。

第七章:利用新的默认 G1 垃圾回收器

在上一章中,我们考察了Java ShellJShell),Java 9 的新读取-评估-打印循环REPL)命令行工具。我们从关于该工具的简介开始,并仔细研究了读取-评估-打印循环的概念。我们花费了大量时间回顾 JShell 命令和命令行选项。我们的内容涵盖了反馈模式、资产列出和壳中编辑的实际指南。我们还获得了使用脚本的经验。

在本章中,我们将深入探讨垃圾回收及其在 Java 9 中的处理方式。我们将从垃圾回收的概述开始,然后探讨 Java 9 之前的特定细节。在掌握这些基础知识后,我们将查看 Java 9 平台中的具体垃圾回收更改。最后,我们将探讨一些即使在 Java 9 之后仍然存在的问题。

本章节涵盖了以下主题:

  • 垃圾回收概述

  • Java 9 之前的垃圾回收方案

  • 使用新的 Java 平台收集垃圾

  • 持续问题

垃圾回收概述

垃圾回收是 Java 中用来释放未使用内存的机制。本质上,当创建一个对象时,会分配内存空间并专门用于该对象,直到没有任何引用指向它。那时,系统会释放内存。Java 会自动为我们执行垃圾回收,这可能导致对内存使用缺乏关注,以及内存管理和系统性能方面的不良编程实践。

Java 的垃圾回收被认为是一种自动内存管理方案,因为程序员不需要指定对象为准备释放的对象。垃圾回收在低优先级线程上运行,正如你将在本章后面读到的,它有可变的执行周期。

在我们关于垃圾回收的概述中,我们将探讨以下概念:

  • 对象生命周期

  • 垃圾回收算法

  • 垃圾回收选项

  • 与垃圾回收相关的 Java 方法

我们将在接下来的章节中逐一探讨这些概念。

对象生命周期

为了完全理解 Java 的垃圾回收,我们需要查看对象的整个生命周期。因为 Java 中垃圾回收的核心是自动的,所以将“垃圾回收”和“内存管理”视为对象生命周期的假设组件并不罕见。

我们将开始回顾对象生命周期,从对象创建开始。

对象创建

对象的声明和创建。当我们编写对象声明或声明对象时,我们是在声明一个名称或标识符,以便我们可以引用对象。例如,以下代码行声明myObjectName为类型CapuchinMonkey的对象名称。此时,没有创建对象,也没有为其分配内存:

    CapuchinMonkey myObjectName;

我们使用new关键字创建对象。以下示例说明了如何调用new操作来创建对象。此操作的结果是:

    myObjectName = new CapuchinMonkey();

当然,我们可以通过使用CapuchinMonkey myObjectName = new CapuchinMonkey();而不是CapuchinMonkey myObjectName;myObjectName = new CapuchinMonkey();来合并声明和创建语句。前面的示例是为了说明目的而分开的。

当创建对象时,为存储该对象分配特定数量的内存。分配的内存量可能因架构和 JVM 而异。

接下来看看对象的中期。

对象中期

对象被创建,Java 为存储该对象分配系统内存。如果该对象未被使用,分配给它的内存被认为是浪费的。这是我们想要避免的事情。即使是小型应用程序,这种类型的浪费内存也可能导致性能下降,甚至出现内存不足的问题。

我们的目标是释放或释放我们不再需要的任何先前分配的内存。幸运的是,在 Java 中,有一个处理此问题的机制。它被称为垃圾回收。

当一个对象,例如我们的myObjectName示例,不再有任何引用指向它时,系统将重新分配相关的内存。

对象销毁

Java 有一个垃圾回收器在代码的暗影中运行(通常是一个低优先级线程)并释放当前分配给未引用对象的内存的想法是吸引人的。那么,这是如何工作的呢?垃圾回收系统监控对象,并在可行的情况下,计算每个对象的引用数量。

当一个对象没有引用时,无法通过当前运行的代码访问它,因此释放相关的内存是合理的。

内存泄漏这个术语指的是丢失或不当释放的小内存块。这些泄漏可以通过 Java 的垃圾回收避免。

垃圾回收算法

Java 虚拟机可以使用几种垃圾回收算法或类型。在本节中,我们将介绍以下垃圾回收算法:

  • 标记和清除

  • CMS 垃圾回收

  • 序列垃圾回收

  • 并行垃圾回收

  • G1 垃圾回收

标记和清除

Java 的初始垃圾回收算法标记和清除使用了一个简单的两步过程:

  1. Java 的第一步,标记,是遍历所有具有可访问引用的对象,将这些对象标记为存活。

  2. 第二步,清除,涉及扫描未标记的任何对象。

如您所容易确定的,标记和清除算法似乎有效,但由于这种方法的两步性质,可能不是非常高效。这最终导致 Java 垃圾回收系统效率大幅提高。

并发标记清除(CMS)垃圾回收

垃圾回收的并发标记清除CMS)算法使用多个线程扫描堆内存。类似于标记和清除方法,它标记要删除的对象,然后进行清除以实际删除这些对象。这种垃圾回收方法本质上是对标记和清除方法的升级。它被修改以利用更快的系统并提高了性能。

要手动调用应用程序的并发标记清除垃圾回收算法,请使用以下命令行选项:

-XX:+UseConcMarkSweepGC 

如果你想使用并发标记清除垃圾回收算法并指定要使用的线程数,你可以使用以下命令行选项。在以下示例中,我们正在告诉 Java 平台使用并发标记清除垃圾回收算法并使用八个线程:

-XX:ParallelCMSThreads=8

串行垃圾回收

Java 的串行垃圾回收在单个线程上工作。在执行时,它会冻结所有其他线程,直到垃圾回收操作完成。由于串行垃圾回收的线程冻结特性,它仅适用于非常小的程序。

要手动调用应用程序的串行垃圾回收算法,请使用以下命令行选项:

-XX:+UseSerialGC

并行垃圾回收

在 Java 9 之前,并行垃圾回收算法是默认的垃圾回收器。它使用多个线程,但在垃圾回收功能完成之前,应用程序中的所有非垃圾回收线程都会被冻结,就像串行垃圾回收算法一样。

G1 垃圾回收

G1 垃圾回收算法是为使用大内存堆而创建的。这种方法涉及将内存堆分割成区域。使用 G1 算法进行垃圾回收时,每个堆区域都并行进行。

G1 算法的另一部分是,当内存被释放时,堆空间会被压缩。不幸的是,压缩操作使用的是停止世界方法。

G1 垃圾回收算法也根据需要收集最多垃圾的区域进行优先级排序。

G1 名称指的是垃圾优先。

要手动调用应用程序的 G1 垃圾回收算法,请使用以下命令行选项:

-XX:+UseG1GC

垃圾回收选项

这里是 JVM 尺寸选项列表:

尺寸描述 JVM 选项标志
设置初始堆大小(年轻代空间加老年代空间)。 -XX:InitialHeapSize=3g
设置最大堆大小(年轻代空间加老年代空间)。 -XX:MaxHeapSize=3g
设置初始和最大堆大小(年轻代空间加老年代空间)。 -Xms2048m -Xmx3g
设置年轻代空间初始大小。 -XX:NewSize=128m
设置年轻代空间最大大小。 -XX:MaxNewSize=128m
设置年轻代大小。使用年轻代与老年代的比例。在右侧的示例标志中,3 表示年轻代将是老年代的三分之一。 -XX:NewRation=3
设置单存活空间大小为 Eden 空间大小的百分比。 -XX:SurvivorRatio=15
设置永久空间的初始大小。 -XX:PermSize=512m
设置永久空间的最大大小。 -XX:MaxPermSize=512m
设置每个线程专用的堆栈区域大小,以 bytes 为单位。 -Xss512k
设置每个线程专用的堆栈区域大小,以 Kbytes 为单位。 -XX:ThreadStackSize=512
设置 JVM 可用的最大堆外内存大小。 -XX:MaxDirectMemorySize=3g

这里是年轻代垃圾收集选项列表:

年轻代垃圾收集调优选项 标志
设置对象从年轻代晋升到老年代之前收集的初始次数。这被称为 晋升阈值 -XX:Initial\TenuringThreshold=16
设置晋升阈值的最大值。 -XX:Max\TenuringThreshold=30
设置允许在年轻代分配的最大对象大小。如果一个对象大于最大大小,它将被分配到老年代,并绕过年轻代。 -XX:Pretenure\SizeThreshold=3m
可以用于将所有在年轻代收集中存活的年轻对象晋升到老年代。 -XX:+AlwaysTenure
使用此标记,只要存活空间有足够的空间,年轻代中的对象就不会晋升到老年代。 -XX:+NeverTenure
我们可以指示我们希望在年轻代中使用线程本地分配块。这是默认启用的。 -XX:+UseTLAB
切换此选项以允许 JVM 适应性地调整线程的 TLAB (Thread Local Allocation Blocks) 的大小。 -XX:+ResizeTLAB
设置线程的 TLAB 初始大小。 -XX:TLABSize=2m
设置 TLAB 的最小允许大小。 -XX:MinTLABSize=128k

这里是 并发标记清除 (CMS) 调优选项列表:

CMS 调优选项 标志
指示您只想使用占用率作为启动 CMS 收集操作的准则。 -XX:+UseCMSInitiating\OccupancyOnly
设置启动 CMS 收集周期时 CMS 生成占用的百分比。如果您指示一个负数,您是在告诉 JVM 您想使用 CMSTriggerRatio -XX:CMSInitiating\OccupancyFraction=70
设置 CMS 生成占用百分比,您希望为此启动 CMS 收集以启动收集统计信息。 -XX:CMSBootstrap\Occupancy=10
这是 CMS 生成中 MinHeapFreeRatio 百分比在 CMS 周期开始前分配的百分比。 -XX:CMSTriggerRatio=70
设置 CMS 永久生成中 MinHeapFreeRatio 百分比在启动 CMS 收集周期前分配的百分比。 -XX:CMSTriggerPermRatio=90
这是触发 CMS 收集后的等待时间。使用此参数指定 CMS 允许等待年轻收集的时间长度。 -XX:CMSWaitDuration=2000
启用并行 remark。 -XX:+CMSParallelRemarkEnabled
启用并行 remark 的幸存空间。 -XX:+CMSParallelSurvivorRemarkEnabled
您可以使用此选项在 remark 阶段之前强制进行年轻收集。 -XX:+CMSScavengeBeforeRemark
使用此选项以防止在 Eden 使用量低于阈值时调度 remark。 -XX:+CMSScheduleRemarkEdenSizeThreshold
设置您希望 CMS 尝试调度 remark 暂停的 Eden 占用百分比。 -XX:CMSScheduleRemarkEdenPenetration=20
这是在年轻代占用达到您想要调度 remark 的大小的 1/4 之前,您想要开始采样 Eden 顶部的位置。 -XX:CMSScheduleRemarkSamplingRatio=4
您可以选择 remark 之后的variant=1variant=2的验证。 -XX:CMSRemarkVerifyVariant=1
选择使用并行算法进行年轻空间收集。 -XX:+UseParNewGC
启用并发阶段的多线程使用。 -XX:+CMSConcurrentMTEnabled
设置用于并发阶段的并行线程数。 -XX:ConcGCThreads=2
设置您想要用于stop-the-world阶段的并行线程数。 -XX:ParallelGCThreads=2
您可以启用增量 CMSiCMS)模式。 -XX:+CMSIncrementalMode
如果此选项未启用,CMS 将不会清理永久空间。 -XX:+CMSClassUnloadingEnabled
这允许System.gc()触发并发收集而不是完整的垃圾收集周期。 -XX:+ExplicitGCInvokesConcurrent
这允许System.gc()触发永久空间的并发收集。 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses

iCMS增量并发标记清除)模式适用于 CPU 数量较少的服务器。它不应在现代硬件上使用。

这里有一些杂项垃圾收集选项:

杂项垃圾收集选项 标志
这将导致 JVM 忽略应用程序对System.gc()方法的任何调用。 -XX:+DisableExplicitGC
这是堆中每 MB 空闲空间(软引用)的存活时间(以毫秒计)。 -XX:SoftRefLRUPolicyMSPerMB=2000
这是抛出OutOfMemory错误之前限制垃圾收集时间的使用策略 -XX:+UseGCOverheadLimit
这限制了在抛出OutOfMemory错误之前垃圾收集所花费的时间比例。这通常与GCHeapFreeLimit一起使用。 -XX:GCTimeLimit=95
这设置了在抛出OutOfMemory错误之前,完整垃圾收集后空闲空间的最低百分比。这通常与GCTimeLimit一起使用。 -XX:GCHeapFreeLimit=5

最后,这里有一些 G1 特定的选项。请注意,这些选项从 JVM 6u26 开始都得到了支持:

G1 垃圾回收选项 标志
堆区域的大小。默认值为 2,048,可接受的范围是 1 MiB 到 32 MiB。 -XX:G1HeapRegionSize=16m
这是暂停预测启发式算法的置信系数。 -XX:G1ConfidencePercent=75
这决定了堆中的最小预留。 -XX:G1ReservePercent=5
这是每个 MMU 的垃圾回收时间片(以毫秒为单位)。 -XX:MaxGCPauseMillis=100
这是每个 MMU 的暂停间隔时间片(以毫秒为单位)。 -XX:GCPauseIntervalMillis=200

MiB代表Mebibyte,是数字信息的字节倍数。

与垃圾回收相关的 Java 方法

让我们看看与垃圾回收相关的两个特定方法。

System.gc()方法

尽管 Java 中的垃圾回收是自动的,但你可以通过显式调用java.lang.System.gc()方法来帮助调试过程。此方法不接受任何参数,也不返回任何值。这是一个显式调用,它会运行 Java 的垃圾回收器。以下是一个示例实现:

    System.gc();
    System.out.println("Garbage collected and unused 
     memory has been deallocated.");

让我们看看一个更深入的例子。在以下代码中,我们首先通过Runtime myRuntime = Runtime.getRuntime();创建Runtime的一个实例,这返回一个单例。这使我们能够访问 JVM。在打印一些标题信息和初始内存统计信息之后,我们创建了一个大小为300000ArrayList。然后,我们创建一个循环,生成100000个数组列表对象。最后,我们在三个遍历中提供输出,在每次遍历之间请求 JVM 调用垃圾回收器,每次暂停1秒。以下是源代码:

    package MyGarbageCollectionSuite;

    import java.util.ArrayList;
    import java.util.concurrent.TimeUnit;

    public class GCVerificationTest 
    {
      public static void main(String[] args) throws 
       InterruptedException 
       {
         // Obtain a Runtime instance (to communicate
          with the JVM)
         Runtime myRuntime = Runtime.getRuntime();

         // Set header information and output initial 
          memory stats
         System.out.println("Garbage Collection
          Verification Test");
         System.out.println("-----------------------------
          -----------------------------");
         System.out.println("Initial JVM Memory: " + 
          myRuntime.totalMemory() + 
            "\tFree Memory: " + myRuntime.freeMemory());

         // Use a bunch of memory
         ArrayList<Integer> AccountNumbers = new 
          ArrayList<>(300000);
         for (int i = 0; i < 100000; i++)
         {
           AccountNumbers = new ArrayList<>(3000);
           AccountNumbers = null;
         }

         // Provide update with with three passes
         for (int i = 0; i < 3; i++)
         {
           System.out.println("---------------------------
            -----------");
           System.out.println("Free Memory before
            collection number " + 
              (i+1) + ": " + myRuntime.freeMemory());
           System.gc();
           System.out.println("Free Memory after
            collection number " + 
              (i+1) + ": " + myRuntime.freeMemory());
           TimeUnit.SECONDS.sleep(1); // delay thread 
            1 second
         }

       }

    }

如您从以下输出中看到的,垃圾回收器在第一次甚至第二次遍历中并没有重新分配所有的'垃圾':

使用System.gc()方法调用垃圾回收器有一个替代方案。在我们的例子中,我们可以使用myRuntime.gc(),我们之前的单例示例。

finalize()方法

你可以将 Java 的垃圾回收器想象成一个死亡使者。当它从内存中移除某个东西时,它就消失了。这个所谓的死亡使者并非没有同情心,因为它为每个方法提供了最后的遗言。对象通过finalize()方法给出他们的最后遗言。如果一个对象有finalize()方法,垃圾回收器在对象被移除和关联内存释放之前会调用它。此方法不接受任何参数,返回类型为void

finalize()方法只调用一次,其运行时可能会有所不同。当然,该方法在移除之前会被调用,但垃圾回收器何时运行取决于系统。例如,如果你有一个相对较小的应用程序,在运行内存丰富的系统上,垃圾回收器可能根本不会运行。那么,为什么还要包含一个finalize()方法呢?重写finalize()方法被认为是一种不良的编程实践。尽管如此,如果需要,你仍然可以使用该方法。实际上,你可以在那里添加代码,添加对对象的引用以确保它不会被垃圾回收器移除。再次强调,这并不建议。

因为 Java 中的所有对象,即使是你自己创建的对象,都是java.lang.Object的子类,所以 Java 中的每个对象都有一个finalize()方法。

尽管垃圾回收器非常复杂,但它可能不会以你想要的方式关闭数据库、文件或网络连接。如果你的应用程序在对象被回收时需要特定的考虑,你可以重写对象的finalize()方法。

这里有一个示例实现,演示了当你可能想要重写对象的finalize()方法时的用例:

    public class Animal 
    {
      private static String animalName;
      private static String animalBreed;
      private static int objectTally = 0;

      // constructor
      public Animal(String name, String type) 
      {
        animalName = name;
        animalBreed = type;

       // increment count of object
        ++objectTally;
      }

      protected void finalize()
      {
        // decrement object count each time this method
        // is called by the garbage collector
        --objectTally;

        //Provide output to user
        System.out.println(animalName + " has been 
         removed from memory.");

        // condition for 1 animal (use singular form)
        if (objectTally == 1) 
        {
          System.out.println("You have " + objectTally + "
           animal remaining.");
        }

        // condition for 0 or greater than 1 
         animals (use plural form)
        else 
        {
          System.out.println("You have " + objectTally + "
           animals remaining.");
        }

      }

    }

如前述代码所示,每当创建一个类型为Animal的对象时,objectTally计数就会增加,而当垃圾回收器移除一个对象时,计数就会减少。

重写对象的finalize()方法通常是不被推荐的。finalize()方法通常应该声明为protected

Java 9 之前的垃圾回收

Java 的垃圾回收并非 Java 9 的新特性,它自 Java 初始发布以来就存在。Java 长期以来一直拥有一个复杂的垃圾回收系统,它是自动的,在后台运行。我们所说的后台运行,是指垃圾回收过程在空闲时间运行。

空闲时间指的是输入/输出之间的时间,例如键盘输入、鼠标点击和输出生成之间。

这种自动垃圾回收一直是开发者选择 Java 作为编程解决方案的关键因素之一。其他编程语言,如 C#和 Objective-C,在 Java 平台成功之后也实现了垃圾回收。

在我们查看 Java 9 平台垃圾回收变化之前,先让我们看一下以下列出的概念:

  • 可视化垃圾回收

  • Java 8 的垃圾回收升级

  • 案例研究 - 使用 Java 编写的游戏

可视化垃圾回收

可视化垃圾回收的工作原理以及它的重要性可能会有所帮助。考虑以下代码片段,它逐步创建字符串Garbage

    001 String var = new String("G");
    002 var += "a";
    003 var += "r";
    004 var += "b";
    005 var += "a";
    006 var += "g";
    007 var += "e";
    008 System.out.println("Your completed String
     is: " + var + ".");

显然,前述代码按照以下方式生成提供的输出:

    Your completed String is Garbage.

可能不清楚的是,示例代码导致五个未引用的字符串对象。这部分原因是字符串是不可变的。正如您在下图中可以看到的,随着每一行代码的连续执行,引用的对象被更新,并且一个额外的对象变为未引用:

图片

列出的先前未引用的对象当然不会打破内存银行,但它表明大量未引用对象可以迅速积累。

Java 8 中的垃圾回收升级

截至 Java 8,默认的垃圾回收算法是并行垃圾回收器。Java 8 的发布带来了一些对 G1 垃圾回收系统的改进。这些改进之一是能够使用以下命令行选项通过删除重复的字符串值来优化堆内存:

-XX:+UseStringDeduplication

G1 垃圾回收器可以在看到字符串时查看字符数组。然后它将值存储起来,并使用一个新的弱引用将字符数组存储起来。如果 G1 垃圾回收器找到一个具有相同哈希码的字符串,它将逐字符比较这两个字符串。如果找到匹配项,两个字符串最终都会指向同一个字符数组。具体来说,第一个字符串将指向第二个字符串的字符数组。

此方法可能需要大量的处理开销,并且只有在被认为有益或绝对必要时才应使用。

案例研究 - 使用 Java 编写的游戏

多玩家游戏需要广泛的管理技术,包括服务器和客户端系统。JVM 以低优先级线程运行垃圾回收线程,并定期运行。服务器管理员以前使用现在已废弃的-Xincgc命令行选项来避免服务器过载时发生的服务器停滞。目标是让垃圾回收更频繁地运行,并且每次执行周期都更短。

在考虑内存使用和垃圾回收时,尽可能在目标系统上使用最少的内存,并在可行范围内将垃圾回收的暂停时间限制到最小。这些技巧对于需要实时性能的游戏、模拟和其他应用程序尤为重要。

JVM 管理存储 Java 内存的堆。JVM 默认以一个小堆开始,随着新对象的创建而增长。堆分为两个部分——年轻代和旧代。当对象最初创建时,它们在年轻代中创建。持久对象会被移动到旧代。对象的创建通常非常快,只需指针增量即可。年轻代的处理速度比旧代快得多。这很重要,因为它适用于整个应用程序,或者在我们的情况下,是一个游戏的效率。

对于我们来说,监控游戏的内存使用情况和垃圾回收发生时变得很重要。为了监控垃圾回收,我们可以在启动游戏时添加详细标志(-verbose:gc),如下例所示:

java -verbose:gc MyJavaGameClass

JVM 将为每次垃圾回收提供一行格式化的输出。以下是详细 GC 输出的格式:

    [<TYPE> <MEMORY USED BEFORE> -> MEMORY USED AFTER
     (TOTAL HEAP SIZE), <TIME>]

让我们看看两个例子。在这个第一个例子中,我们看到GC类型指的是我们之前讨论过的年轻代分区:

    [GC 31924K -> 29732K(42234K), 0.0019319 secs]

在这个第二个例子中,Full GC表示对内存堆的持久代进行了垃圾回收操作:

    [Full GC 29732K -> 10911K(42234K), 0.0319319 secs]

您可以使用-XX:+PrintGCDetails选项从垃圾收集器获取更多详细信息,如下所示:

java -verbose:gc -XX:+PrintGCDetails MyJavaGameClass

使用新的 Java 平台收集垃圾

Java 一开始就提供了自动垃圾回收功能,使其成为许多程序员的开发平台选择。在其它编程语言中避免手动内存管理变得司空见惯。我们已经深入研究了垃圾回收系统,包括 JVM 使用的各种方法或算法。Java 9 对垃圾回收系统进行了一些相关更改,并成为三个Java 增强计划JEP)问题的焦点。这些问题如下所示:

  • 默认垃圾回收(JEP 248)

  • 废弃的垃圾回收组合(JEP 214)

  • 统一垃圾回收日志(JEP 271)

我们将在以下章节中回顾这些垃圾回收概念及其相应的Java 增强计划JEP)问题。

默认垃圾回收

我们之前详细介绍了 Java 9 之前 JVM 使用的以下垃圾回收方法。这些仍然是可能的垃圾回收算法:

  • CMS 垃圾回收

  • 串行垃圾回收

  • 并行垃圾回收

  • G1 垃圾回收

让我们简要回顾一下这些方法:

  • CMS 垃圾回收:CMS 垃圾回收算法使用多个线程扫描堆内存。使用这种方法,JVM 标记对象以供删除,然后进行清理以实际删除它们。

  • 串行垃圾回收:这种方法在单个线程上使用无线程冻结模式。当垃圾回收正在进行时,它会冻结所有其他线程,直到垃圾回收操作完成。由于串行垃圾回收的线程冻结特性,它仅适用于非常小的程序。

  • 并行垃圾回收:这种方法使用多个线程,但在垃圾回收功能完成之前,冻结应用程序中的所有非垃圾回收线程,就像串行垃圾回收算法一样。

  • G1 垃圾回收:这是具有以下特性的垃圾回收算法:

    • 用于大内存堆

    • 涉及将内存堆分割成区域

    • 与每个堆区域并行进行

    • 在内存释放时压缩堆空间

    • 使用停止世界方法进行压缩操作

    • 根据收集垃圾量最多的区域进行优先级排序

在 Java 9 之前,并行垃圾收集算法是默认的垃圾收集器。在 Java 9 中,G1 垃圾收集器是 Java 内存管理系统的新的默认实现。这对于 32 位和 64 位服务器配置都适用。

Oracle 评估认为,由于 G1 垃圾收集器的低暂停特性,它比并行方法是一个更好的垃圾收集方法。这一变化基于以下概念:

  • 限制延迟很重要

  • 最大化吞吐量不如限制延迟重要

  • G1 垃圾收集算法是稳定的

在将 G1 垃圾收集方法作为默认方法而非并行方法时,涉及两个假设:

  • 将 G1 作为默认的垃圾收集方法将显著增加其使用。这种增加的使用可能会揭示在 Java 9 之前未意识到的性能或稳定性问题。

  • 与并行方法相比,G1 方法对处理器的需求更高。在某些用例中,这可能会有些问题。

表面上看,这个变化可能对 Java 9 来说是一个巨大的进步,而且这完全可能是事实。然而,在盲目接受这个新的默认收集方法时,应该保持谨慎。建议在切换到 G1 时测试系统,以确保您的应用程序不会因性能下降或由 G1 使用引起的不预期的问题而受到影响。如前所述,G1 没有像并行方法那样受益于广泛的测试。

关于缺乏广泛测试的最后一个观点具有重要意义。在 Java 9 中将 G1 作为默认的自动内存管理(垃圾收集)系统,相当于将开发者变成了不知情的测试者。虽然预期不会有重大问题,但知道在使用 Java 9 时 G1 可能存在性能和稳定性问题,这将使测试您的 Java 9 应用程序更加重要。

废弃的垃圾收集组合

Oracle 在从 Java 平台的新版本中删除功能、API 和库之前,一直很擅长废弃它们。有了这个方案,Java 8 中废弃的语言组件将在 Java 9 中面临被移除的问题。有一些垃圾收集组合被认为很少使用,并在 Java 8 中被废弃。这些组合,如下所示,已在 Java 9 中被移除:

  • DefNew + CMS

  • ParNew + SerialOld

  • 增量 CMS

这些组合,除了很少使用外,还给垃圾收集系统引入了一个不必要的复杂性级别。这导致了对系统资源的额外消耗,而没有为用户或开发者提供相应的利益。

以下列出的垃圾收集配置受到了 Java 8 平台上述弃用的 影响:

垃圾收集配置 标志
DefNew + CMS -XX:+UseParNewGC``-XX:UseConcMarkSweepGC
ParNew + SerialOld -XX:+UseParNewGC
ParNew + iCMS -Xincgc
ParNew + iCMS -XX:+CMSIncrementalMode``-XX:+UseConcMarkSweepGC
Defnew + ICMS -XX:+CMSIncrementalMode``-XX:+UseConcMarkSweepGC``-XX:-UseParNewGC

Java 增强计划 214JEP 214)移除了 JDK 8 中弃用的垃圾收集组合。这些组合在上文列出,并列出了控制这些组合的标志。此外,移除了启用 CMS 前台收集的标志,并且不在 JDK 9 中存在。以下列出了这些标志:

垃圾收集组合 标志
CMS foreground -XX:+UseCMSCompactAtFullCollection
CMS foreground -XX+CMSFullGCsBeforeCompaction
CMS foreground -XX+UseCMSCollectionPassing

移除已弃用的垃圾收集组合的唯一不利之处在于,使用本节中列出的任何标志的 JVM 启动文件的应用程序,需要修改它们的 JVM 启动文件以删除或替换旧标志。

统一垃圾收集日志

Java 增强计划#271JEP-271)的标题为“统一 GC 日志”,旨在重新实现使用之前在 JEP-158 中引入的统一 JVM 日志框架的垃圾收集日志。因此,让我们首先回顾一下统一 JVM 日志(JEP-158)。

统一 JVM 日志(JEP-158)

为 JVM 创建一个统一的日志架构是 JEP-158 的核心目标。以下是 JEP 的目标的高层次列表:

  • 为所有日志操作创建一个 JVM 范围的命令行选项集

  • 使用分类的标签进行日志记录

  • 提供六个日志级别:

    • 错误

    • 警告

    • 信息

    • 调试

    • 跟踪

    • 开发

这不是目标列表的详尽无遗。我们将在第十四章命令行标志中更详细地讨论 JEP-158。

在日志的背景下,对 JVM 的更改可以分为:

  • 标签

  • 级别

  • 装饰

  • 输出

  • 命令行选项

让我们简要地看看这些类别。

标签

日志标签在 JVM 中标识,如果需要,可以在源代码中更改。标签应该是自我标识的,例如gc代表垃圾收集。

级别

每条日志消息都有一个关联的级别。如前所述,级别包括错误、警告、信息、调试、跟踪和开发。以下图表显示了级别在日志记录的信息量方面的递增程度:

图片

装饰

在 Java 9 的日志框架的背景下,装饰是日志消息的元数据。以下是可用的装饰的字母顺序列表:

  • level

  • pid

  • tags

  • tid

  • 时间

  • timemillis

  • timenanos

  • uptime

  • uptimemillis

  • uptimenanos

对于这些装饰的解释,请参阅第十四章,命令行标志

输出

Java 9 日志框架支持三种类型的输出:

  • stderr:提供输出到标准错误

  • stdout:提供输出到标准输出

  • 文本文件:将输出写入文本文件

命令行选项

日志框架中添加了一个新的命令行选项,以提供对 JVM 日志操作的总体控制。-Xlog命令行选项具有广泛的参数和可能性。以下是一个示例:

-Xlog:gc+rt*=debug

在此示例中,我们正在告诉 JVM 执行以下操作:

  • 记录所有带有至少gcrt标签的消息

  • 使用debug级别

  • 提供输出到stdout

统一 GC 日志(JEP-271)

现在我们对 Java 9 的日志框架的更改有了大致的了解,让我们看看 JEP-271 引入了哪些变化。在本节中,我们将探讨以下领域:

  • 垃圾回收日志选项

  • gc标签

  • 其他注意事项

垃圾回收日志选项

在介绍 Java 9 的日志框架之前,以下是我们可以使用的垃圾回收日志选项和标志列表:

垃圾回收日志选项 JVM 选项标志
这将打印基本的垃圾回收信息。 -verbose:gc-XX:+PrintGC
这将打印更详细的垃圾回收信息。 -XX:+PrintGCDetails
您可以为每个垃圾回收事件打印时戳。秒是连续的,从 JVM 启动时间开始。 -XX:+PrintGCTimeStamps
您可以为每个垃圾回收事件打印日期戳。示例格式:2017-07-26T03:19:00.319+400:[GC . . . ] -XX:+PrintGCDateStamps
您可以使用此标志来打印单个垃圾回收工作线程任务的时戳。 -XX:+PrintGC\TaskTimeStamps
使用此标志可以将垃圾回收输出重定向到文件而不是控制台。 -Xloggc:
您可以在每次收集周期后打印有关年轻空间的详细信息。 -XX:+Print\TenuringDistribution
您可以使用此标志来打印 TLAB 分配统计信息。 -XX:+PrintTLAB
使用此标志,您可以在stop-the-world暂停期间打印引用处理(即弱、软等)的时间。 -XX:+PrintReferenceGC
这将报告垃圾回收是否正在等待原生代码取消内存中对象的固定。 -XX:+PrintJNIGCStalls
这将在每次stop-the-world暂停后打印暂停摘要。 -XX:+PrintGC\ApplicationStoppedTime
此标志将打印垃圾回收每个并发阶段的耗时。 -XX:+PrintGC\ApplicationConcurrentTime
使用此标志将在完整垃圾回收后打印类直方图。 -XX:+Print\ClassHistogramAfterFullGC
使用此标志将在完全垃圾回收之前打印类直方图。 -XX:+Print\ClassHistogramBeforeFullGC
这将在完全垃圾回收后创建堆转储文件。 -XX:+HeapDump\AfterFullGC
这将在完全垃圾回收之前创建堆转储文件。 -XX:+HeapDump\BeforeFullGC
这将在内存不足的情况下创建堆转储文件。 -XX:+HeapDump\OnOutOfMemoryError
你使用此标志来指定你想要在系统上保存堆转储文件的路径。 -XX:HeapDumpPath=<path>
你可以使用此选项来打印 CMS 统计信息,if n >= 1。仅适用于 CMS。 -XX:PrintCMSStatistics=2
这将打印 CMS 初始化的详细信息。仅适用于 CMS。 -XX:+Print\CMSInitiationStatistics
你可以使用此标志来打印有关空闲列表的附加信息。仅适用于 CMS。 -XX:PrintFLSStatistics=2
你可以使用此标志来打印有关空闲列表的附加信息。仅适用于 CMS。 -XX:PrintFLSCensus=2
你可以使用此标志来打印晋升(年轻到终身)失败后的详细诊断信息。仅适用于 CMS。 -XX:+PrintPromotionFailure
此标志允许你在晋升(年轻到终身)失败时,输出有关 CMS 旧代状态的有用信息。仅适用于 CMS。 -XX:+CMSDumpAt\PromotionFailure
当使用-XX:+CMSDumpAt\PromotionFailure标志时,你可以使用-XX:+CMSPrint\ChunksInDump来包含有关空闲块的其他详细信息。仅适用于 CMS。 -XX:+CMSPrint\ChunksInDump
当使用-XX:+CMSPrint\ChunksInDump标志时,你可以通过使用-XX:+CMSPrint\ObjectsInDump标志来包含有关分配对象的附加信息。仅适用于 CMS。 -XX:+CMSPrint\ObjectsInDump

gc 标签

我们可以使用gc标签与-Xlog选项一起使用,以通知 JVM 仅以 info 级别记录带有gc标签的项目。如您所回忆,这与使用-XX:+PrintGC类似。使用这两个选项,JVM 将为每次垃圾收集操作记录一行。

重要提示:gc标签不是为了单独使用而设计的;相反,建议与其他标签一起使用。

我们可以创建宏来向我们的垃圾收集日志添加逻辑。以下是日志宏的一般语法:

    log_<level>(Tag1[,...])(fmtstr, ...)

这里是一个日志宏的示例:

    log_debug(gc, classloading)("Number of objects
     loaded: %d.", object_count)

以下示例骨架日志宏展示了如何使用新的 Java 9 日志框架创建脚本以实现更精确的日志记录:

    LogHandle(gc, rt, classunloading) log;
    if (log.is_error())
    {
      // do something specific regarding the 'error' level
    }

    if (log.is_warning())
    {
      // do something specific regarding the 'warning'
      level
    }

    if (log.is_info())
    {
      // do something specific regarding the 'info' level
    }

    if (log.is_debug())
    {
      // do something specific regarding the 'debug' level
    }

    if (log.is_trace())
    {
      // do something specific regarding the 'trace' level
    }

其他考虑因素

在垃圾收集日志方面,以下是一些需要考虑的附加项:

  • 使用新的-Xlog:gc应该会产生与-XX:+PrintGCDetails命令行选项和标志配对类似的结果

  • 新的trace级别提供了与verbose标志之前提供的详细程度相同的信息

持续问题

即使 Java 9 已经问世,Java 的垃圾回收系统也存在一些缺点。因为它是一个自动过程,我们无法完全控制垃圾回收器何时运行。作为开发者,我们无法控制垃圾回收,这是由 JVM 控制的。JVM 决定何时运行垃圾回收。正如您在本章前面所见,我们可以使用System.gc()方法请求 JVM 运行垃圾回收。尽管我们使用了这种方法,但我们无法保证我们的请求会被尊重,或者会及时得到响应。

在本章前面,我们回顾了几个垃圾回收的方法和算法。我们讨论了作为开发者,我们如何控制这个过程。这假设我们有控制垃圾回收的能力。即使我们指定了特定的垃圾回收技术,例如使用-XX:+UseConcMarkSweepGC进行 CMS 垃圾回收,我们也无法保证 JVM 会使用该实现。因此,我们可以尽我们所能控制垃圾收集器的工作方式,但应该记住,JVM 在如何、何时以及是否进行垃圾回收方面拥有最终决定权。

我们对垃圾回收缺乏完全的控制强调了编写考虑内存管理的有效代码的重要性。在接下来的几节中,我们将探讨如何编写代码,以显式地使对象有资格被 JVM 进行垃圾回收。

使对象有资格进行垃圾回收

使对象可用于垃圾回收的简单方法是将引用变量指向对象的引用赋值为null。让我们回顾这个例子:

    package MyGarbageCollectionSuite;

    public class GarbageCollectionExperimentOne 
    {
      public static void main(String[] args) 
      {

        // Declare and create new object.
        String junk = new String("Pile of Junk");

        // Output to demonstrate that the object
        has an active reference
        // and is not eligible for garbage collection.
        System.out.println(junk);

        // Set the reference variable to null.
        junk = null;

        // The String object junk is now eligible
        for garbage collection.

      }

    }

如代码注释所示,一旦字符串对象引用变量被设置为null,在这种情况下使用junk = null;语句,对象就可供垃圾回收。

在我们的下一个例子中,我们将通过将引用变量设置为指向不同的对象来放弃一个对象。正如您在下面的代码中所见,这会导致第一个对象可供垃圾回收:

    package MyGarbageCollectionSuite;

    public class GarbageCollectionExperimentTwo
    {
      public static void main(String[] args)
      {
        // Declare and create the first object.
        String junk1 = new String("The first pile of
         Junk");

        // Declare and create the second object.
        String junk2 = new String("The second pile of 
         Junk");

        // Output to demonstrate that both objects have
        active references
        // and are not eligible for garbage collection.
        System.out.println(junk1);
        System.out.println(junk2);

        // Set the first object's reference to the
         second object.
        junk1 = junk2;

        // The String "The first pile of Junk" is now
         eligible for garbage collection.

      }

    }

让我们回顾一下使对象可用于垃圾回收的另一种方法。在这个例子中,我们有一个单个实例变量(objectNbr),它是一个指向GarbageCollectionExperimentThree类实例的引用变量。这个类除了创建指向GarbageCollectionExperimentThree类实例的额外引用变量外,没有做任何有趣的事情。在我们的例子中,我们将objectNbr2objectNbr3objectNbr4objectNbr5引用设置为null。尽管这些对象有实例变量并且可以相互引用,但通过将它们的引用设置为null,它们在类外的可访问性已经被终止。这使得它们(objectNbr2objectNbr3objectNbr4objectNbr5)有资格进行垃圾回收:

    package MyGarbageCollectionSuite;
    {

      // instance variable
      GarbageCollectionExperimentThree objectNbr;

      public static void main(String[] args) 
      {
        GarbageCollectionExperimentThree objectNbr2 = new
         GarbageCollectionExperimentThree();
        GarbageCollectionExperimentThree objectNbr3 = new
         GarbageCollectionExperimentThree();
        GarbageCollectionExperimentThree objectNbr4 = new
         GarbageCollectionExperimentThree();
        GarbageCollectionExperimentThree objectNbr5 = new
         GarbageCollectionExperimentThree();
        GarbageCollectionExperimentThree objectNbr6 = new
         GarbageCollectionExperimentThree();
        GarbageCollectionExperimentThree objectNbr7 = new
         GarbageCollectionExperimentThree();

        // set objectNbr2 to refer to objectNbr3
        objectNbr2.objectNbr = objectNbr3;

        // set objectNbr3 to refer to objectNbr4
        objectNbr3.objectNbr = objectNbr4;

        // set objectNbr4 to refer to objectNbr5
        objectNbr4.objectNbr = objectNbr5;

        // set objectNbr5 to refer to objectNbr2
        objectNbr5.objectNbr = objectNbr2;

        // set selected references to null
        objectNbr2 = null;
        objectNbr3 = null;
        objectNbr4 = null;
        objectNbr5 = null;

      }

    }

摘要

在本章中,我们对垃圾回收作为 Java 9 平台前一个关键组件进行了深入回顾。我们的回顾包括对象生命周期、垃圾回收算法、垃圾回收选项以及与垃圾回收相关的方法。我们研究了 Java 8 中垃圾回收的升级,并分析了案例研究以帮助我们理解现代垃圾回收。然后,我们将注意力转向了与新的 Java 9 平台相关的垃圾回收更改。我们对 Java 9 中的垃圾回收的探索包括了对默认垃圾回收、已弃用的垃圾回收组合和统一垃圾回收日志的考察。通过查看即使在 Java 9 之后仍然存在的几个垃圾回收问题,我们结束了对垃圾回收的探索。

在下一章中,我们将探讨如何使用Java 微基准工具JMH),这是一个用于为 JVM 编写基准测试的 Java 工具库,来编写性能测试。

第八章:使用 JMH 进行微基准测试的应用

在上一章中,我们深入回顾了垃圾回收,包括对象生命周期、垃圾回收算法、垃圾回收选项以及与垃圾回收相关的方法。我们简要地看了 Java 8 中垃圾回收的升级,并专注于新 Java 9 平台的变化。我们对 Java 9 中的垃圾回收进行了探索,包括默认垃圾回收、已弃用的垃圾回收组合、统一的垃圾回收日志以及即使在 Java 9 之后仍然存在的垃圾回收问题。

在本章中,我们将探讨如何使用Java 微基准工具JMH)编写性能测试,这是一个用于编写针对Java 虚拟机JVM)基准的 Java 工具库。我们将使用 Maven 和 JMH 来帮助说明使用新 Java 9 平台进行微基准测试的强大功能。

具体来说,我们将涵盖以下主题:

  • 微基准测试概述

  • 使用 Maven 进行微基准测试

  • 基准测试选项

  • 避免微基准测试陷阱的技术

微基准测试概述

微基准测试用于测试系统的性能。这与宏观基准测试不同,宏观基准测试在不同的平台上运行测试以比较效率和进行后续分析。在微基准测试中,我们通常针对一个系统上的特定代码片段进行测试,例如一个方法或循环。微基准测试的主要目的是在我们的代码中识别优化机会。

基准测试有多种方法,我们将专注于使用 JMH 工具。那么,为什么要进行基准测试呢?开发者并不总是关心性能问题,除非性能是一个明确的要求。这可能导致部署后的意外,如果微基准测试作为开发过程的一部分进行,这些意外本可以避免。

微基准测试发生在过程的几个阶段。如图所示,该过程涉及设计、实现、执行、分析和增强:

图片

设计阶段,我们确定我们的目标和相应地设计微基准测试。在实现阶段,我们编写微基准测试,然后在执行阶段实际运行测试。有了微基准测试的结果,我们在分析阶段解释和分析结果。这导致在增强阶段对代码进行改进。一旦我们的代码被更新,我们重新设计微基准测试,调整实现,或者直接进入执行阶段。这是一个循环过程,直到我们达到目标中确定的性能优化。

使用 JMH 的方法

Oracle 的文档表明,最理想的 JMH 使用案例是使用依赖于应用程序 JAR 文件的 Maven 项目。他们还建议,微基准测试应通过命令行进行,而不是在集成开发环境IDE)内进行,因为这可能会影响结果。

Maven,也称为 Apache Maven,是一个项目管理和理解工具,我们可以用它来管理我们的应用程序项目构建、报告和文档。

要使用 JMH,我们将使用字节码处理器(注解)来生成基准代码。我们使用 Maven 存档来启用 JMH。

为了测试 JMH,我们需要一个支持 Maven 和 Java 9 的 IDE。如果您还没有 Java 9 或支持 Java 9 的 IDE,您可以按照下一节中的步骤进行操作。

安装 Java 9 和具有 Java 9 支持的 Eclipse

您可以从 JDK 9 早期访问构建页面下载并安装 Java 9——jdk.java.net/9/

安装 Java 9 后,下载 Eclipse 的最新版本。在撰写本书时,那是 Oxygen。以下是相关链接——www.eclipse.org/downloads/

下一步是在您的 IDE 中启用 Java 9 支持。启动 Eclipse Oxygen 并选择帮助 | Eclipse Marketplace...,如图所示:

图片

当 Eclipse Marketplace 对话框窗口打开时,使用搜索框搜索Java 9 支持。如图所示,您将看到一个安装按钮:

图片

在安装过程中,您将需要接受许可协议,并在完成后,您将需要重新启动 Eclipse。

实践实验

现在我们已经将 Eclipse 更新为支持 Java 9,您可以运行快速测试以确定 JMH 是否在您的开发计算机上工作。首先,创建一个新的 Maven 项目,如图所示:

图片

接下来,我们需要添加一个依赖项。我们可以通过直接编辑pom.xml文件来实现,以下是相应的代码:

    <dependency>
      <groupId>org.openjdk.jmh</groupId>
      <artifactId>jmh-core</artifactId>
      <version>0.1</version>
    </dependency>

或者,我们可以使用依赖项标签页,在对话框窗口中输入数据,如图所示。使用此表单将更新pom.xml文件中的上述代码:

图片

接下来,我们需要编写一个包含 JMH 方法的类。这只是一个初始测试,以确认我们最近更新的开发环境。以下是您可以用于测试的示例代码:

    package com.packt.benchmark.test.com.packt.benchmark.test;

    import org.open.jdk.jmh.Main;

    public class Test 
    {

      public static void main(String[] args)
      {
        Main.main(args);
      }
    }

现在,我们可以编译并运行我们的非常简单的测试程序。结果在控制台标签页中提供,或者如果您使用的是命令行,则是实际的控制台。以下是您将看到的内容:

图片

你可以看到程序运行得足够好,足以让我们知道 JMH 正在工作。当然,正如输出所示,没有设置基准。我们将在下一节中处理这个问题。

使用 Maven 进行微基准测试

开始使用 JMH 的一个方法是通过 JMH Maven 原型。第一步是创建一个新的 JMH 项目。在我们的系统命令提示符中,我们将输入mvn命令,后面跟着一系列参数来创建一个新的 Java 项目和必要的 Maven pom.xml文件:

mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jmh -DarchetypeArtifactId=jmh -java-benchmark-archetype -DgroupId=com.packt -DartifactId=chapter8-benchmark -Dversion=1.0

一旦你输入mvn命令和前面的详细参数,你将通过终端看到报告给你的结果。根据你的使用水平,你可能会看到来自repo.maven.apache.org/maven2/org/apache/mave/plugins和其他类似仓库站点的大量下载。

你还会看到一个信息部分,它会告诉你关于项目构建过程的信息:

图片

很可能还会从repo.maven.apache.org仓库下载额外的插件和其他资源。然后,你将看到一个信息反馈组件,它会让你知道项目正在批量模式下生成:

图片

最后,你将看到一个参数集和一个说明,表明你的项目构建成功。正如以下示例所示,整个过程不到 21 秒就完成了:

图片

将根据我们在-DartifactId选项中包含的参数创建一个文件夹。在我们的例子中,我们使用了-DartifactId=chapter8-benchmark,Maven 创建了一个chapter8-benchmark项目文件夹:

图片

你将看到 Maven 创建了pom.xml文件以及一个源(src)文件夹。在该文件夹中,位于C:\chapter8-benchmark\src\main\java\com\packt的子目录结构下是MyBenchmark.java文件。Maven 为我们创建了一个基准类:

图片

这是 JMH Maven 项目创建过程中创建的MyBenchmark.java类的内容:

    /*
     * Copyright (c) 2014, Oracle America, Inc.
     * All rights reserved.
     *
     * Redistribution and use in source and binary forms, with or 
       without
     * modification, are permitted provided that the following 
       conditions are met:
     *
     * * Redistributions of source code must retain the above
         copyright notice,
     * this list of conditions and the following disclaimer.
     *
     * * Redistributions in binary form must reproduce the above 
         copyright
     * notice, this list of conditions and the following
       disclaimer in the
     * documentation and/or other materials provided with the 
       distribution.
     *
     * * Neither the name of Oracle nor the names of its 
         contributors may be used
     * to endorse or promote products derived from this software 
       without
     * specific prior written permission.
     *
     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 
       CONTRIBUTORS "AS IS"
     * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
       LIMITED TO, THE
     * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
       PARTICULAR PURPOSE
     * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
       CONTRIBUTORS BE
     * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
       EXEMPLARY, 
       OR
     * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
       PROCUREMENT OF
     * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
       OR BUSINESS
     * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
       WHETHER IN
     * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
       OTHERWISE)
     * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
       ADVISED OF
     * THE POSSIBILITY OF SUCH DAMAGE.
     */

    package com.packt;

    import org.openjdk.jmh.annotations.Benchmark;

    public class MyBenchmark 
    {
      @Benchmark
      public void testMethod() 
      {

        // This is a demo/sample template for building your JMH 
           benchmarks. 
        //Edit as needed.
        // Put your benchmark code here.
      }
    }

我们下一步是修改testMethod(),使其有可测试的内容。以下是我们将用于基准测试的修改后的方法:

    @Benchmark
    public void testMethod() 
    {
      int total = 0;
      for (int i=0; i<100000; i++)
      {
        total = total + (i * 2 );
      }
      System.out.println("Total: " + total);
    }

在我们的代码编辑完成后,我们将导航回项目文件夹,例如在我们的例子中,回到C:\chapter8-benchmark,并在命令提示符下执行mvn clean install

你将看到几个仓库下载、源编译、插件安装,最后是如这里所示的成功构建指示器:

图片

现在,你将在项目目录中看到.classpath.project文件,以及新的.settings和目标子文件夹:

图片

如果你导航到\target子文件夹,你会看到我们的benchmarks.jar文件已被创建。这个 JAR 文件包含我们运行基准测试所需的内容。

我们可以在 IDE 中更新我们的MyBenchmark.java文件,例如 Eclipse。然后,我们可以再次执行mvn clean install来覆盖我们的文件。在初始时间之后,我们的构建将会快得多,因为不需要下载任何内容。以下是除了第一次之外构建过程的输出:

图片

我们的最后一步是运行基准工具。我们可以使用以下命令来执行--java -jar benchmarks.jar。即使是针对简单代码的小型基准测试,就像我们的例子一样,基准测试可能需要一些时间来运行。可能会进行几个迭代,包括预热,以提供更简洁和有效的基准测试结果集。

我们的基准测试结果在此提供。正如你所见,测试运行了 8 分钟 8 秒:

图片

基准测试选项

在前面的章节中,你学习了如何运行基准测试。在本节中,我们将查看运行基准测试的可配置选项:

  • 模式

  • 时间单位

模式

我们之前章节中基准测试结果的输出包括一个模式列,其值为thrpt,它是吞吐量的缩写。这是默认模式,还有另外四种模式。所有 JMH 基准测试模式都列在下面,并作如下描述:

模式 描述
所有 测量所有其他模式,包括它们。
平均 此模式测量单个基准运行的平均时间。
样本时间 此模式测量基准测试执行时间,包括最小和最大时间。
单次射击时间 使用此模式,没有 JVM 预热,测试是为了确定单个基准方法运行所需的时间。
吞吐量 这是默认模式,测量基准测试每秒可以运行的操作数。

要指定要使用的基准测试模式,你需要修改你的@Benchmark代码行,如下所示:

    @Benchmark @BenchmarkMode(Mode.All)
    @Benchmark @BenchmarkMode(Mode.Average)
    @Benchmark @BenchmarkMode(Mode.SamplmeTime)
    @Benchmark @BenchmarkMode(Mode.SingleShotTime)
    @Benchmark @BenchmarkMode(Mode.Throughput)

时间单位

为了在基准测试输出中获得更高的精确度,我们可以指定一个特定的时间单位,以下是从短到长的列表:

  • NANOSECONDS

  • MICROSECONDS

  • MILLISECONDS

  • SECONDS

  • MINUTES

  • HOURS

  • DAYS

为了进行此指定,我们只需将以下代码添加到我们的@Benchmark行:

    @Benchmark @BenchmarkMode(Mode.Average) 
    @OutputTimeUnit(TimeUnit.NANOSECONDS)

在前面的例子中,我们指定了平均模式和纳秒作为时间单位。

避免微基准测试陷阱的技术

微基准测试不是每个开发者都必须担心的事情,但对于那些需要的人来说,有一些陷阱你应该注意。在本节中,我们将回顾最常见的陷阱,并提出避免它们的策略。

管理电力

有许多子系统可以帮助您管理功率和性能之间的平衡(即cpufreq)。这些系统可以改变基准测试期间的时间状态。

针对这个陷阱,有两种建议的策略:

  • 在运行测试之前禁用任何电源管理系统

  • 运行更长时间的基准测试

操作系统调度器

操作系统调度器,如 Solaris 调度器,有助于确定哪些软件进程可以访问系统资源。使用这些调度器可能导致不可靠的基准测试结果。

针对这个陷阱,有两种建议的策略:

  • 优化您的系统调度策略

  • 运行更长时间的基准测试

时间共享

时间共享系统用于帮助平衡系统资源。使用这些系统通常会导致线程启动和停止时间之间的不规则间隔。此外,CPU 负载将不会均匀,我们的基准测试数据对我们来说将不那么有用。

避免这个陷阱有两种建议的策略:

  • 在运行基准测试之前测试所有代码,以确保一切按预期工作

  • 使用 JMH 在所有线程启动后或所有线程停止后进行测量

消除死代码和常量折叠

死代码和常量折叠通常被称为冗余代码,而我们的现代编译器在消除它们方面相当出色。死代码的一个例子是永远不会被执行的代码。考虑以下示例:

    . . . 

    int value = 10;

    if (value != null)
    {
      System.out.println("The value is " + value + ".");
    } else 
      {
         System.out.println("The value is null."); // This is
         a line of Dead-Code
    }

    . . . 

在我们前面的例子中,被识别为死代码的行永远不会被执行,因为变量的值永远不会等于 null。它在条件if语句评估变量之前立即被设置为10

问题在于,在尝试消除死代码的过程中,基准测试代码有时会被移除。

常量折叠是在编译时约束被替换为实际结果时发生的编译器操作。编译器执行常量折叠以消除任何冗余的运行时计算。在以下示例中,我们有一个final int后面跟着一个基于涉及第一个int的数学计算的第二个int

    . . . 

    static final int value = 10;

    int newValue = 319 * value;

    . . . 

常量折叠操作会将前面代码的两行转换为以下内容:

    int newValue = 3190;

针对这个陷阱,有一种建议的策略:

  • 使用 JMH API 支持来确保您的基准测试代码不会被消除

运行到运行的变化

在基准测试中,有许多问题可能会极大地影响运行到运行的变化。

针对这个陷阱,有两种建议的策略:

  • 在每个子系统内多次运行 JVM

  • 使用多个 JMH 人员

缓存容量

动态随机存取存储器DRAM)非常慢。这可能导致基准测试期间非常不同的性能结果。

针对这个陷阱,有两种建议的策略:

  • 使用不同的问题集运行多个基准测试。在测试期间跟踪您的内存占用。

  • 使用@State注解来指定 JMH 状态。此注解用于定义实例的作用域。有三个状态:

    • Scope.Benchmark:实例在运行相同测试的所有线程之间共享。

    • Scope.Group:每个线程组分配一个实例。

    • Scope.Thread:每个线程将拥有自己的实例。这是默认状态。

摘要

在本章中,我们了解到 JMH 是一个用于为 JVM 编写基准测试的 Java 工具库。我们通过使用 Maven 和 JMH 来编写性能测试,帮助说明使用新 Java 9 平台进行微基准测试的流程。我们从微基准测试概述开始,然后深入探讨了使用 Maven 进行微基准测试,回顾了基准测试选项,并以一些避免微基准测试陷阱的技术作为总结。

在下一章中,我们将学习编写一个管理其他进程并利用 Java 9 平台现代进程管理 API 的应用程序。

第九章:利用 ProcessHandle API

在上一章中,我们发现了 Java 微基准测试工具JMH)。我们探讨了性能测试以及如何使用 JMH(Java 虚拟机基准测试的 Java 库)编写它们。我们从微基准测试的概述开始,然后查看使用 Maven 进行微基准测试,回顾基准测试选项,并以避免微基准测试陷阱的技术结束。

在本章中,我们将重点关注 Process 类的更新和新的 java.lang.ProcessHandle API。在 Java 9 之前,Java 中管理进程从未容易过,因为 Java 很少被用来自动化控制其他进程。API 缺乏一些功能,一些任务需要以系统特定的方式解决。例如,在 Java 8 中,给进程访问其自己的 进程标识符PID)是一个不必要的困难任务。

在本章中,读者将获得编写管理其他进程并利用 Java 现代进程管理 API 的应用程序所需的所有知识。

在本章中,我们将涵盖以下主题:

  • 什么是 ProcessHandle 接口以及如何使用它

  • 如何获取当前进程的 PID

  • 如何列出操作系统中运行的不同进程

  • 如何有效地等待外部进程完成

  • 如何终止外部进程

进程是什么?

在本节中,我们将回顾在 Java 应用程序编程的上下文中进程是什么。如果你已经熟悉进程,你可能考虑跳过这一节。

进程是操作系统中的执行单元。当你启动一个程序时,你就启动了一个进程。当机器启动代码时,它首先执行的是启动进程。然后,这个进程启动其他进程,这些进程成为启动进程的子进程。这些子进程可能再启动其他进程。这样,当机器运行时,就有进程树在运行。当机器执行某事时,它是在某个进程内部执行的代码中完成的。操作系统也以几个同时执行的过程运行。应用程序作为单个或多个进程执行。大多数应用程序作为单个进程运行,但以一个例子来说,Chrome 浏览器启动了几个进程来完成所有的渲染和网络通信操作,最终作为一个浏览器工作。

要更好地了解进程是什么,请在 Windows 上启动任务管理器或在 OS X 上启动活动监视器,然后点击进程标签。你将看到机器上当前存在的不同进程。使用这些工具,你可以查看进程的参数,或者你可以终止单个进程。

每个进程都有为其工作分配的内存,并且不允许它们自由访问彼此的内存。

操作系统调度执行的执行单元是一个线程。一个进程由一个或多个线程组成。这些线程由操作系统调度器调度,并在时间槽中执行。

每个操作系统都有进程标识符,这是一个标识进程的数字。在任何时候,两个进程都不能同时活跃,共享相同的 PID。当我们想要在操作系统中识别一个活动进程时,我们使用 PID。在 Linux 和其他类 Unix 操作系统中,kill命令用于终止进程。要传递给此程序的参数是要终止的进程的 PID。终止可以是优雅的,就像请求进程退出一样。如果进程决定不退出,它可以继续运行。程序可以准备在收到此类请求时停止。例如,Java 应用程序可以添加一个调用Runtime.getRuntime().addShutdownHook(Thread t)方法的Thread对象。传递的线程应该在进程被请求停止时启动,并且线程可以执行程序退出前必须完成的全部任务。然而,没有保证它一定会启动。这取决于实际的实现。

新的ProcessHandle接口

在 Java 9 中,有两个新的接口及其实现支持操作系统进程的处理。其中之一是ProcessHandle,另一个是ProcessHandle.Info,它是前者的嵌套接口。

ProcessHandle对象标识一个操作系统进程,并提供管理进程的方法。在 Java 的早期版本中,这只能通过使用 PID 来识别进程的操作系统特定方法来实现。这种方法的主要问题是 PID 仅在进程活动时是唯一的。当进程结束时,操作系统可以自由地重新使用 PID 为新进程分配。当我们只知道进程的 PID 并检查该进程是否仍在运行时,我们实际上是在检查是否存在具有该 PID 的活动进程。当我们检查时,我们的进程可能仍然存活,但当我们下次查询进程状态时,它可能是一个不同的进程。

桌面和服务器操作系统会尽可能长时间地不重复使用 PID 值。在某些嵌入式系统中,操作系统可能只使用 16 位来存储 PID。当只使用 16 位值时,PID 被重复使用的可能性更大。我们可以使用ProcessHandle API 来避免这个问题。我们可以接收一个ProcessHandle并调用handle.isAlive()方法。当进程结束时,此方法将返回false。即使 PID 被重复使用,这也同样有效。

获取当前进程的 PID

我们可以通过句柄访问进程的 PID。handle.getPid() 方法返回一个表示 PID 数值的 Long。由于通过句柄访问进程更安全,因此此方法的重要性有限。当我们的代码想要向某些其他管理工具提供关于自身的信息时,它可能很有用。程序创建一个以数值 PID 命名的文件是一种常见做法。可能存在某些程序不能在多个进程中运行的要求。在这种情况下,代码将自身的 PID 文件写入特定目录。如果已存在同名 PID 文件,则处理停止。如果先前的进程崩溃并终止而没有删除 PID 文件,那么系统管理员可以轻松删除该文件并启动新进程。如果程序挂起,那么如果系统管理员知道 PID,则可以轻松地杀死该死进程。

要获取当前进程的 PID,可以使用调用链 ProcessHandle.current(). getPid().

获取进程信息

要获取关于进程的信息,我们需要访问进程的 Info 对象。这可以通过 ProcessHandle 来获取。我们通过调用 handle.info() 方法来返回它。

Info 接口定义了查询方法,用于提供关于进程的信息。这些方法是:

  • command() 返回一个包含用于启动进程的命令的 Optional<String>

  • arguments() 返回一个包含在命令行上用于启动进程的参数的 Optional<String[]>

  • commandLine() 返回一个包含整个命令行的 Optional<String>

  • startInstant() 返回一个 Optional<Instant>,它本质上表示进程启动的时间

  • totalCpuDuration() 返回一个 Optional<Duration>,它表示进程自启动以来使用的 CPU 时间

  • user() 返回一个包含属于该进程的用户名称的 Optional<String>

这些方法返回的值都是 Optional,因为没有保证实际操作系统或 Java 实现可以返回这些信息。然而,在大多数操作系统上,它应该可以工作,并且返回的值应该是存在的。

以下示例代码显示了给定进程的信息:

    import java.io.IOException;
    import java.time.Duration;
    import java.time.Instant;
    public class ProcessHandleDemonstration
    {
      public static void main(String[] args) throws 
       InterruptedException, IOException
      {
        provideProcessInformation(ProcessHandle.current());
        Process theProcess = new
         ProcessBuilder("SnippingTool.exe").start();
        provideProcessInformation(theProcess.toHandle());
        theProcess.waitFor();
        provideProcessInformation(theProcess.toHandle());
      }
      static void provideProcessInformation(ProcessHandle theHandle)
      {
        // get id
        long pid = ProcessHandle.current().pid();
        // Get handle information (if available)
        ProcessHandle.Info handleInformation = theHandle.info();
        // Print header
        System.out.println("|=============================|");
        System.out.println("| INFORMATION ON YOUR PROCESS |");
        System.out.println("|=============================|\n");
        // Print the PID
        System.out.println("Process id (PID): " + pid);
        System.out.println("Process Owner: " + 
          handleInformation.user().orElse(""));
        // Print additional information if available
        System.out.println("Command:" + 
         handleInformation.command().orElse(""));
        String[] args = handleInformation.arguments().orElse
         (new String[]{});
        System.out.println("Argument(s): ");
        for (String arg: args) System.out.printf("\t" + arg);
        System.out.println("Command line: " + handleInformation.
         commandLine().orElse(""));
        System.out.println("Start time: " + 
         handleInformation.startInstant().
         orElse(Instant.now()).toString());
        System.out.printf("Run time duration: %sms%n",
         handleInformation.totalCpuDuration()
         .orElse(Duration.ofMillis(0)).toMillis());
      }
    }

以下是上述代码的控制台输出:

图片

列出进程

在 Java 9 之前,我们没有获取活动进程列表的方法。随着 Java 9 的推出,现在可以通过流来获取进程。有三个方法返回 Stream<ProcessHandle>。一个列出子进程。另一个列出所有后代;子进程及其子进程的子进程。第三个列出所有进程。

列出子进程

要获取可以用来控制子进程的进程句柄流,应使用静态方法 processHandle.children()。这将创建由 processHandle 表示的进程的后代进程的快照并创建 Stream。由于进程是动态的,因此无法保证在代码执行期间,当我们的程序处理句柄时,所有子进程都仍然是活动的。其中一些可能已终止,我们的进程可能产生了新的子进程,可能来自不同的线程。因此,代码不应假设流中的每个 ProcessHandle 元素代表一个活跃且正在运行的进程。

以下程序在 Windows 中启动 10 个命令提示符,然后计算子进程的数量并将其打印到标准输出:

    package packt.mastering.java9.process;

    import java.io.IOException;

    public class ChildLister {
      public static void main(String[] args) throws IOException {
        for (int i = 0; i < 10; i++) {
          new ProcessBuilder().command("cmd.exe").start();
        }
        System.out.println("Number of children :" +
         ProcessHandle.current().children().count());
      }
    }

执行程序将产生以下结果:

列出后代

列出后代与列出子进程非常相似,但如果我们调用 processHandle.descendants() 方法,则 Stream 将包含所有子进程以及这些子进程的后代进程,依此类推。以下程序以命令行参数启动命令提示符,以便它们也生成另一个终止的 cmd.exe

    package packt.mastering.java9.process;

    import java.io.IOException;
    import java.util.stream.Collectors;

    public class DescendantLister {
      public static void main(String[] args) throws IOException {
        for (int i = 0; i < 10; i++) {
          new ProcessBuilder().command("cmd.exe","/K","cmd").                
           start();
        }
        System.out.println("Number of descendants: " +
         ProcessHandle.current().descendants().count();
      }
    }

执行命令几次将导致以下非确定性的输出:

输出清楚地表明,当创建后代的 Stream 时,并非所有进程都是活动的。示例代码启动了 10 个进程,每个进程又启动了另一个。Stream 中没有 20 个元素,因为其中一些子进程在处理过程中已终止。

列出所有进程

列出所有进程与列出后代和子进程略有不同。方法 allProcess() 是静态的,并在执行时返回一个包含操作系统所有活动进程句柄的 Stream

以下示例代码将看似 Java 进程的进程命令打印到控制台:

    package packt.mastering.java9.process;
    import java.lang.ProcessHandle.Info;
    public class ProcessLister {
      private static void out(String format, Object... params) {
        System.out.println(String.format(format, params));
      }
      private static boolean looksLikeJavaProcess(Info info) {
        return info.command().isPresent() &&
         info.command().get().
         toLowerCase().indexOf("java") != -1;
      }

      public static void main(String[] args) {
        ProcessHandle.allProcesses().
         map(ProcessHandle::info).
         filter(info -> looksLikeJavaProcess(info)).
         forEach(
           (info) -> System.out.println(
             info.command().orElse("---"))
         );
      }

    }

程序的输出列出了包含字符串 java 的所有进程命令:

您的实际输出可能当然不同。

等待进程

当一个进程启动另一个进程时,它可能需要多次等待该进程,因为它需要另一个程序的结果。如果可以以某种方式组织任务结构,使得父程序可以在等待子进程完成时做其他事情,那么父进程可以在进程句柄上调用 isAlive() 方法。很多时候,父进程直到派生的进程完成都没有事情可做。旧应用程序实现了循环,调用 Thread.sleep() 方法,这样就不会过度浪费 CPU,并且不时检查进程是否仍然存活。Java 9 提供了一种更好的等待进程的方法。

ProcessHandle接口有一个名为onExit的方法,该方法返回一个CompletableFuture。这个类是在 Java 8 中引入的,使得在没有循环的情况下等待任务完成成为可能。如果我们有一个进程的句柄,我们可以简单地调用handle.onExit().join()方法来等待进程结束。返回的CompletableFuture对象的get()方法将返回最初用于创建它的ProcessHandle实例。

我们可以在句柄上多次调用onExit()方法,每次都会返回一个不同的CompletableFuture对象,每个对象都与同一个进程相关。我们可以在对象上调用cancel()方法,但它只会取消CompletableFuture对象,而不会取消进程,并且对由同一个ProcessHandle实例创建的其他CompletableFuture对象也没有任何影响。

终止进程

要终止一个进程,我们可以在ProcessHandle实例上调用destroy()方法或destroyForcibly()方法。这两个方法都会终止进程。destroy()方法预期会优雅地终止进程,执行进程关闭序列。在这种情况下,如果实际实现支持进程的优雅、正常终止,则会执行添加到运行时的关闭钩子。destroyForcibly()方法将强制进程终止,在这种情况下,关闭序列将不会执行。

如果由句柄管理的进程已经不活跃,那么当代码调用这些方法中的任何一个时,都不会发生任何事情。如果在调用句柄上的onExit()方法时创建了任何CompletableFuture对象,那么在进程终止后调用destroy()destroyForcefully()方法时,它们将会完成。这意味着CompletableFuture对象将在进程终止完成后的一段时间后从join()或类似方法返回,而不是在destroy()destroyForcefully()返回后立即返回。

还很重要的一点是,进程的终止可能取决于许多因素。如果实际等待终止另一个进程的进程没有终止另一个进程的权限,那么请求将失败。在这种情况下,方法的返回值是false。同样重要的是要理解,返回值为true并不意味着进程实际上已经终止。它只意味着操作系统已接受终止请求,操作系统将在未来的某个时刻终止进程。这实际上会很快发生,但不是瞬间发生,因此如果在destroy()destroyForcefully()返回true后的一段时间内,isAlive()方法返回true,这不应感到惊讶。

destroy()destroyForcefully() 之间的区别是具体实现相关。Java 标准并没有声明 destroy() 会终止进程并执行关闭序列。它只是 请求终止进程。由这个 ProcessHandle 对象表示的进程是否 正常终止 取决于具体实现(download.java.net/java/jdk9/docs/api/java/lang/ProcessHandle.html#supportsNormalTermination--)*。

要了解更多关于 ProcessHandle 接口的信息,请访问 download.java.net/java/jdk9/docs/api/java/lang/ProcessHandle.html

这是因为某些操作系统没有实现优雅的进程终止功能。在这种情况下,destroy() 的实现与调用 destroyForcefully() 相同。接口 ProcessHandle 的系统特定实现必须实现 supportsNormalTermination() 方法,该方法仅在实现支持正常(非强制)进程终止时返回 true。该方法预期对所有实际调用返回相同的值,并且在 JVM 实例执行期间不应更改返回值。不需要多次调用该方法。

以下示例演示了进程启动、进程终止和等待进程终止。在我们的示例中,我们使用了两个类。第一个类演示了 .sleep() 方法:

    package packt.mastering.java9.process; 

    public class WaitForChildToBeTerminated  
    { 
      public static void main(String[] args) 
       throws InterruptedException  
      { 
        Thread.sleep(10_000); 
      } 
    } 

我们示例中的第二个类调用了 WaitForChildToBeTerminated 类:

    package packt.mastering.java9.process;

    import java.io.IOException;
    import java.util.Arrays;
    import java.util.concurrent.CompletableFuture;
    import java.util.stream.Collectors;

    public class TerminateAProcessAfterWaiting {
      private static final int N = 10;

      public static void main(String[] args)
       throws IOException, InterruptedException {  
         ProcessHandle ph[] = new ProcessHandle[N];

         for (int i = 0; i < N; i++)  
         {
           final ProcessBuilder pb = ew ProcessBuilder(). 
            command("java", "-cp", "build/classes/main",
            "packt.mastering.java9.process.
            WaitForChildToBeTerminated");
           Process p = pb.start();
           ph[i] = p.toHandle();
         }
         long start = System.currentTimeMillis();
         Arrays.stream(ph).forEach(ProcessHandle::destroyForcibly);

         CompletableFuture.allOf(Arrays.stream(ph).
          map(ProcessHandle::onExit).
          collect(Collectors.toList()).
          toArray(new CompletableFuture[ph.length])).
          join();
         long duration = System.currentTimeMillis() - start;
         System.out.println("Duration " + duration + "ms");
      }
    }

上述代码启动了 10 个进程,每个进程执行一个睡眠 10 秒的程序。然后强制销毁这些进程,更具体地说,操作系统被要求销毁它们。我们的示例将 CompletableFuture 数组组合成一个 CompletableFuture,这些 CompletableFuture 对象是通过各个进程的句柄创建的。

当所有进程完成后,它将打印出测量的时间(以毫秒为单位)。时间间隔从进程创建开始,直到进程创建循环完成。测量时间间隔的结束是当进程被 JVM 通过从 join() 方法返回时识别出来。

样本代码将睡眠时间设置为 10 秒。这是一个更明显的时间段。运行代码两次并删除销毁进程的行会导致打印输出速度大大减慢。实际上,测量和打印的经过时间也会显示终止进程有影响。

一个小型进程控制器应用程序

为了总结并应用我们在本章中学到的所有内容,我们来看一个示例过程控制应用程序。应用程序的功能非常简单。它从一系列配置文件中读取参数,以启动一些进程,然后如果其中任何一个停止,它会尝试重新启动该进程。

即使是从这个演示版本中,也可以创建出实际应用。你可以通过环境变量指定来扩展过程的参数集。你可以为过程添加默认目录,输入和输出重定向,甚至可以指定一个过程在不被控制应用程序终止和重启的情况下允许消耗多少 CPU。

应用程序由四个类组成。

  • Main:这个类包含公共静态void main方法,并用于启动守护进程。

  • Parameters:这个类包含一个过程的配置参数。在这个简单的例子中,它将只包含一个字段,即commandLine。如果应用程序被扩展,这个类将包含默认目录、重定向和 CPU 使用限制数据。

  • ParamsAndHandle:这个类实际上就是一个数据元组,它持有对Parameters对象的引用以及一个进程句柄。当一个进程死亡并重新启动时,进程句柄会被新的句柄替换,但Parameters对象的引用永远不会改变,它是配置。

  • ControlDaemon:这个类实现了Runnable接口,并作为一个独立的线程启动。

在代码中,我们将使用我们在前几节中讨论的大多数进程 API,终止进程,并且我们将使用大量的线程代码和流操作。理解 JVM 的线程工作对于进程管理来说也很重要。然而,当与进程 API 一起使用时,它的重要性被强调了。

主类

主方法从命令行参数中获取目录名称。它将此视为相对于当前工作目录的相对路径。它使用同一类中的另一个单独的方法来从目录中的文件中读取配置集,然后启动控制守护进程。以下代码是程序的main方法:

    public static void main(String[] args) throws IOException, 
     InterruptedException  
    {
      // DemoOutput.out() simulated - implementation no shown
      DemoOutput.out(new File(".").getAbsolutePath().toString());
      if (args.length == 0)    {
        System.err.println("Usage: daemon directory");
        System.exit(-1);
      }
      Set<Parameters> params = parametersSetFrom(args[0]);
      Thread t = new Thread(new ControlDaemon(params));
      t.start();
    }

虽然这是一个守护进程,但我们是以普通线程而不是守护线程的方式启动它的。当一个线程被设置为守护线程时,它不会保持 JVM 存活。当所有其他非守护线程停止时,JVM 将直接退出,守护线程将被停止。在我们的情况下,我们执行的守护线程是唯一一个保持代码运行的那个。在那之后启动了,主线程就没有其他事情可做了,但 JVM 应该保持存活,直到操作员通过发出 Unix kill命令或按命令行上的Control + C来终止它。

使用 JDK 中新的FilesPaths类,获取指定目录中的文件列表以及从文件中获取参数非常简单:

    private static Set<Parameters>  
     GetListOfFilesInDirectory(String directory) throws IOException  
    {
      return Files.walk(Paths.get(directory))
       .map(Path::toFile)
       .filter(File::isFile)
       .map(file -> Parameters.fromFile(file))
       .collect(Collectors.toSet());
    }

我们以 Path 对象的形式获取文件流,将其映射到 File 对象,然后如果配置目录中有目录,我们将其过滤掉,并将剩余的普通文件映射到 Parameters 对象,使用 Parameters 类的静态方法 fromFile。最后,我们返回一个包含这些对象的 Set

参数类

我们的 Parameters 类具有一个字段和一个构造函数,如下所示:

    final String[] commandLine;

    public Parameters(String[] commandLine) {
      this.commandLine = commandLine;
    }

参数类有两个方法。第一个方法 getCommandLineStrings 从属性中获取命令行字符串。这个数组包含命令和命令行参数。如果没有在文件中定义,则返回一个空数组:

    private static String[] getCommandLineStrings(Properties props)  
    {
      return Optional
       .ofNullable(props.getProperty("commandLine"))
       .orElse("")
       .split("\\s+");
    }

第二个方法是 static fromFile,它从属性文件中读取属性:

    public static Parameters fromFile(final File file)  
    {
      final Properties props = new Properties();
      try (final InputStream is = new FileInputStream(file)) {
        props.load(is);
      }  catch (IOException e) {
           throw new RuntimeException(e);
      }
      return new Parameters(getCommandLineStrings(props));
    }

如果程序处理的参数集被扩展,则此类也应进行修改。

ParamsAndHandle

ParamsAndHandle 是一个非常简单的类,包含两个字段。一个用于参数,另一个是用于访问使用参数启动的进程的进程句柄:

    public class ParamsAndHandle  
    {
      final Parameters params;
      ProcessHandle handle;

      public ParamsAndHandle(Parameters params,
        ProcessHandle handle) {
          this.params = params;
          this.handle = handle;
      }

      public ProcessHandle toHandle() {
        return handle;
      }
    }

由于该类与它所使用的 ControlDaemon 类紧密相关,因此没有与字段相关联的修改器或访问器。我们将这两个类视为同一封装边界内的东西。toHandle 方法存在是为了我们可以将其用作方法句柄,正如我们将在下一章中看到的。

ControlDaemon

ControlDaemon 类实现了 Runnable 接口,并以一个单独的线程启动。构造函数获取从属性文件中读取的参数集,并将其转换为 ParamsAndHandle 对象集:

    private final Set<ParamsAndHandle> handlers;

    public ControlDaemon(Set<Parameters> params) {
      handlers = params
      .stream()
      .map( s -> new ParamsAndHandle(s,null))
      .collect(Collectors.toSet());
    }

因为进程此时尚未启动,所以句柄都是 nullrun() 方法启动进程:

    @Override
    public void run() {
      try {
        for (ParamsAndHandle pah : handlers) {
          log.log(DEBUG, "Starting {0}", pah.params);
          ProcessHandle handle = start(pah.params);
          pah.handle = handle;
        }
        keepProcessesAlive();
        while (handlers.size() > 0) {
          allMyProcesses().join();
        } 
      } catch (IOException e)  
        {
          log.log(ERROR, e);
        }
    }

处理过程遍历参数集,并使用(在此类中稍后实现的)方法启动进程。每个进程的句柄都到达 ParamsAndHandle 对象。之后,调用 keepProcessesAlive 方法并等待进程完成。当进程停止时,它将被重启。如果无法重启,它将被从集合中移除。

allMyProcesses 方法(也在此类中实现)返回一个 CompletableFuture,当所有启动的进程都已停止时完成。在 join() 方法返回时,一些进程可能已经被重启。只要至少有一个进程正在运行,线程就应该运行。

使用 CompletableFuture 等待进程和 while 循环,我们使用最小的 CPU 来保持线程存活,只要至少有一个我们管理的进程正在运行,即使经过几次重启也是如此。即使这个线程大部分时间不使用 CPU 且不执行任何代码,我们也必须保持这个线程存活,以便让 keepProcessesAlive() 方法使用 CompletableFutures 来完成其工作。该方法在下面的代码片段中显示:

    private void keepProcessesAlive()  
    {
      anyOfMyProcesses()
       .thenAccept(ignore -> {
         restartProcesses();
         keepProcessesAlive();
       });
    }

keepProcessesAlive() 方法调用 anyOfMyProcesses() 方法,该方法返回一个 CompletableFuture,当任何一个管理进程退出时完成。该方法安排在 CompletableFuture 完成时执行传递给 thenAccept() 方法的 lambda 表达式。lambda 表达式做两件事:

  • 重启已停止的进程(可能只有一个)

  • 调用 keepProcessesAlive() 方法

需要理解的是,此调用不是在 keepProcessesAlive() 方法内部执行的。这不是一个递归调用。这是一个作为 CompletableFuture 行动的调度。我们不是在递归调用中实现循环,因为我们可能会耗尽栈空间。当进程重启时,我们要求 JVM 执行器再次执行此方法。

需要知道的是,JVM 使用默认的 ForkJoinPool 来调度这些任务,并且这个池中包含守护线程。这就是为什么我们必须等待并保持方法运行的原因,因为这是唯一一个非守护线程,它可以防止 JVM 退出。

下一个方法是 restartProcesses()

    private void restartProcesses()  
    {
      Set<ParamsAndHandle> failing = new HashSet<>();
      handlers.stream()
       .filter(pah -> !pah.toHandle().isAlive())
       .forEach(pah -> {
         try {
           pah.handle = start(pah.params);
         } catch (IOException e) {
             failing.add(pah);
         }
       });
       handlers.removeAll(failing);
    }

此方法启动我们管理进程集中的进程,这些进程尚未运行。如果任何重启失败,它会从集中移除失败的进程。(请注意,不要在循环中移除它,以避免 ConcurrentModificationException。)

anyOfMyProcesses()allMyProcesses() 方法使用了辅助的 completableFuturesOfTheProcessesand() 方法,并且很简单:

    private CompletableFuture anyOfMyProcesses()  
    {
      return CompletableFuture.anyOf(
        completableFuturesOfTheProcesses());
    }

    private CompletableFuture allMyProcesses() {
      return CompletableFuture.allOf(
        completableFuturesOfTheProcesses());
    }

completableFuturesOfTheProcesses() 方法返回一个由当前运行的管理的进程调用它们的 onExit() 方法创建的 CompletableFutures 数组。这以紧凑且易于阅读的函数式编程风格完成,如下所示:

    private CompletableFuture[] completableFuturesOfTheProcesses()  
    {
      return handlers.stream()
       .map(ParamsAndHandle::toHandle)
       .map(ProcessHandle::onExit)
       .collect(Collectors.toList())
       .toArray(new CompletableFuture[handlers.size()]);
    }

集合被转换为 stream,映射到 ProcessHandle 对象的 stream(这就是为什么在 ParamsAndHandle 类中需要 toHandle() 方法的原因)。然后,使用 onExit() 方法将句柄映射到 CompletableFuture stream,最后我们将其收集到一个列表中并转换为数组。

完成我们的示例应用程序的最后一个方法是以下内容:

    private ProcessHandle start(Parameters params)
     throws IOException {
       return new ProcessBuilder(params.commandLine)
        .start()
        .toHandle();
    }

此方法使用 ProcessBuilder 启动进程,并返回 ProcessHandle,以便我们可以替换集中的旧进程并管理新进程。

摘要

在本章中,我们讨论了 Java 9 如何更好地帮助我们管理进程。在 Java 9 之前,从 Java 内部管理进程需要特定于操作系统的实现,并且在 CPU 使用和编码实践方面并不理想。现代 API,如 ProcessHandle 类,使得处理进程的几乎所有方面成为可能。我们列出了新的 API,并为每个 API 提供了简单的示例代码。在章节的后半部分,我们组合了一个整个应用程序来管理进程,其中所学的 API 得到了实际应用。

在下一章中,我们将详细探讨与 Java 9 一同发布的 Java Stack Walking API。我们将通过代码示例来说明如何使用该 API。

第十章:精细粒度堆栈跟踪

Java 9 附带了一个新的堆栈遍历 API,允许程序遍历调用栈。这是一个非常特殊的功能,普通程序很少需要。对于一些非常特殊的情况,API 可能很有用——对于由框架提供的功能。因此,如果您想要一种高效的堆栈遍历方法,该方法可以提供可筛选的堆栈跟踪信息,您将喜欢这个新的堆栈遍历 API。

API 提供了快速和优化的对调用栈的访问,实现了对单个帧的懒访问。

在本章中,我们将介绍以下主题:

  • Java 堆栈概述

  • 堆栈信息的重要性

  • 使用StackWalker

  • StackFrame

  • 性能

Java 堆栈概述

在我们深入研究堆栈遍历之前,让我们先从介绍 Java 堆栈开始。这是基本的堆栈信息,并不特定于堆栈遍历。

Java 运行时有一个名为 Stack 的类,可以使用它来存储对象,使用的是后进先出LIFO)策略。

当计算算术表达式时,它们是使用堆栈来完成的。如果我们首先在我们的代码中添加AB,则A会被推送到操作数栈,然后B被推送到操作数栈,最后执行加法操作,该操作从操作数栈的顶部两个元素中获取结果,并将其推送到那里,即A + B

JVM 是用 C 语言编写的,并执行调用 C 函数和从那里返回的操作。这个调用-返回序列是通过使用本地方法栈来维护的,就像任何其他 C 程序一样。

最后,当 JVM 创建一个新的线程时,它也会分配一个包含帧的调用栈,这些帧反过来又包含局部变量、对前一个帧的引用以及对包含执行方法的类的引用。当一个方法被调用时,会创建一个新的帧。当方法完成其执行时,即返回或抛出异常时,帧会被销毁。这个栈,即 Java 虚拟机栈,是堆栈遍历 API 管理的栈。

堆栈信息的重要性

一般而言,当我们想要开发依赖于调用者的代码时,我们需要堆栈信息。有关调用者的信息允许我们的代码根据该信息做出决策。在一般实践中,使功能依赖于调用者不是一个好主意。影响方法行为的信息应通过参数提供。依赖于调用者的代码开发应该相当有限。

JDK 使用原生方法访问堆栈信息,这些原生方法对 Java 应用程序不可用。SecurityManager是一个定义应用程序安全策略的类。这个类检查反射 API 的调用者是否有权访问另一个类的非公共成员。为了做到这一点,它必须能够访问调用者类,它通过一个受保护的本地方法来实现这一点。

这是一个在不遍历堆栈的情况下实施某些安全措施的示例。我们向外部开发者开放我们的代码,让他们将其用作库。我们还调用库用户提供的类的方法,他们可能反过来调用我们的代码。有一些代码我们希望允许库用户调用,但前提是他们不是从我们的代码中调用的。如果我们不希望允许某些代码直接通过库代码访问,我们可以使用 Java 9 的模块结构,不导出包含不调用类包,以此来实现。这就是我们设置额外条件的原因,即代码对来自外部的调用者可用,除非它们是由我们的代码调用的:

示例

另一个例子是我们想要获取对日志记录器的访问权限。Java 应用程序使用许多不同的日志记录器,日志系统通常非常灵活,可以根据实际需要切换不同日志记录器的输出。最常见的方法是为每个类使用不同的日志记录器,日志记录器的名称通常是类的名称。这种做法非常普遍,以至于日志框架甚至提供了接受类引用而不是名称的日志记录器访问方法。这本质上意味着获取日志记录器句柄的调用看起来如下:

    private static final Logger LOG = Logger.getLogger(MyClass.class); 

当我们忘记更改获取新日志记录器调用中的类名时,从现有类创建新类可能会出现问题。这不是一个严重的问题,但很常见。在这种情况下,我们的代码将使用其他类的日志记录器,并且实际上可以工作,但在分析日志文件时可能会造成混淆。如果有一个方法可以返回名为调用者类的日志记录器,那就更好了。

让我们继续在下一两个部分中通过代码片段示例来探索堆栈信息。

示例 - 限制调用者

在本节中,我们开发了一个包含两个方法的示例库。hello() 方法将 hello 打印到标准输出。callMe() 方法接受一个 Runnable 参数并运行它。然而,第一个方法是受限的。它仅在调用者完全在库外部时执行。如果调用者以库调用 Runnable 的方式获得控制权,则会抛出 IllegalCallerException。API 的实现很简单:

    package packt.java9.deep.stackwalker.myrestrictivelibrary; 
    public class RestrictedAPI { 
      public void hello(){ 
        CheckEligibility.itIsNotCallBack(); 
        System.out.println("hello"); 
      } 
      public void callMe(Runnable cb){ 
        cb.run(); 
      } 
    } 

执行资格检查的代码在一个单独的类中实现,以保持简单。我们很快就会检查那段代码,但在那之前,我们来看看我们用来启动演示的主要代码。我们用来演示功能的程序代码如下:

    package packt.java9.deep.stackwalker.externalcode; 

    import
     packt.java9.deep.stackwalker.myrestrictivelibrary.RestrictedAPI; 

    public class DirectCall { 

      public static void main(String[] args) { 
        RestrictedAPI api = new RestrictedAPI(); 
        api.hello(); 
        api.callMe(() -> { 
            api.hello(); 
        }); 
      } 
    } 

此代码创建了我们 API 类的一个实例,然后直接调用 hello() 方法。它应该能正常工作并在屏幕上打印出字符 hello。下一行代码要求 callMe() 方法回调以 lambda 表达式形式提供的 Runnable。在这种情况下,调用将失败,因为调用者在外部,但却是从库内部被调用的。

让我们现在看看如何实现资格检查:

    package packt.java9.deep.stackwalker.myrestrictivelibrary; 

    import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; 

    public class CheckEligibility { 
      private static final String packageName 
        = CheckEligibility.class.getPackageName(); 

      private static boolean notInLibrary(StackWalker.StackFrame f) { 
        return !inLibrary(f); 
      } 

      private static boolean inLibrary(StackWalker.StackFrame f) { 
        return f.getDeclaringClass().getPackageName() 
         .equals(packageName); 
      } 

      public static void itIsNotCallBack() { 
        boolean eligible = StackWalker 
         .getInstance(RETAIN_CLASS_REFERENCE) 
         .walk(s -> s.dropWhile(CheckEligibility::inLibrary) 
           .dropWhile(CheckEligibility::notInLibrary) 
           .count() == 0 
         ); 
         if (!eligible) { 
           throw new IllegalCallerException(); 
         } 
      } 
    } 

itIsNotCallBack() 方法是从 hello() 方法中调用的一个方法。此方法创建了一个栈跟踪器并调用了 walk() 方法。walk() 方法的参数是一个 Function,它将 StackFrame 对象的 Stream 转换为 walk() 方法将返回的其他值。

首先,这个参数设置可能看起来很复杂且难以理解。返回一个提供 StackFrame 对象的 Stream 可能会更合理,而不是强迫调用者定义一个将作为参数获取此值的 Function

示例代码使用 lambda 表达式来定义 Function 作为 walk() 方法的参数。lambda 表达式的参数 s 是流。由于此流的第一个元素是实际调用,所以我们丢弃它。因为这些调用也应该在调用者不符合条件时被拒绝,即使调用 hello() 方法是通过库内部的其他类和方法进行的,我们也丢弃属于 CheckEligibility 类包内的所有帧元素。此包是 packt.java9.deep.stackwalker.myrestrictivelibrary,在代码中这个字符串存储在字段 packageName 中。结果流只包含来自库外部的 StackFrame 对象。我们也丢弃这些,直到流耗尽或直到我们找到一个再次属于库的 StackFrame。如果所有元素都被丢弃,我们就成功了。在这种情况下,count() 的结果为零。如果我们找到 StackFrame 中的某个类属于库,这意味着外部代码是从库中调用的,在这种情况下我们必须拒绝工作。在这种情况下,变量 eligible 将为 false,并且我们抛出一个异常,如以下截图所示:

图片

示例 - 获取调用者的日志记录器

要获取一个日志记录器,Java 9 有一个新的 API。使用此 API,一个模块可以为服务 LoggerFinder 提供一个实现,该服务反过来可以返回实现 getLogger() 方法的 Logger。这消除了库对特定日志记录器或日志记录器外观的依赖,这是一个巨大的优势。但仍然存在一个较小但仍然令人烦恼的问题,需要我们再次将类的名称作为 getLogger() 方法的参数写入。这个问题仍然存在。

为了避免这项繁琐的任务,我们创建了一个辅助类,该类查找调用者类并检索适合调用者类和模块的记录器。因为在这种情况下,我们不需要在堆栈跟踪中引用的所有类,我们将调用 StackWalker 类的 getCallerClass() 方法。我们在包 packt.java9.deep.stackwalker.logretriever 中创建了一个名为 Labrador 的类:

    package packt.java9.deep.stackwalker.logretriever; 

    import java.lang.System.Logger; 
    import java.lang.System.LoggerFinder; 

    import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; 

    public class Labrador { 
      public static Logger retrieve() { 
        final Class clazz = StackWalker 
          .getInstance(RETAIN_CLASS_REFERENCE) 
          .getCallerClass(); 
        return LoggerFinder.getLoggerFinder().getLogger( 
          clazz.getCanonicalName(), clazz.getModule()); 
      } 
    } 

在 Java 9 之前,解决此问题的方法是获取 Thread 类的 StackTrace 数组,并从那里查找调用者类的名称。另一种方法是扩展具有受保护方法 getClassContext()SecurityManager,该方法返回堆栈上所有类的数组。这两种解决方案都会遍历堆栈并组成一个数组,尽管我们只需要数组中的一个元素。在日志检索的情况下,由于记录器通常存储在 private static final 字段中,因此每个类在类初始化期间只初始化一次,因此性能惩罚可能并不显著。在其他用例中,性能惩罚可能非常显著。

在我们看过两个示例之后,我们将查看 StackWalker 内部工作的细节。

使用 StackWalker

在本节中,你将更熟悉如何与 StackWalker 一起工作。在本节中,我们将探讨以下主题:

  • 获取 StackWalker 实例

  • 栈遍历选项

获取 StackWalker 实例

要在栈元素上执行遍历,我们需要一个栈遍历器的实例。为此,我们调用 getInstance() 方法。如图所示,此方法有四个重载版本:

  • static StackWalker getInstance()

  • static StackWalker getInstance(StackWalker.Option option)

  • static StackWalker getInstance(Set<StackWalker.Option> options)

  • static StackWalker getInstance(Set<StackWalker.Option> options, int estimateDepth)

第一个版本不接受任何参数,并返回一个 StackWalker 实例,该实例将允许我们遍历正常的栈帧。这通常是我们感兴趣的。该方法的其他版本接受 StackWalker.Option 值或值。枚举 StackWalker.Option,如名称所示,位于 StackWalker 类中,有三个值:

  • RETAIN_CLASS_REFERENCE

  • SHOW_REFLECT_FRAMES

  • SHOW_HIDDEN_FRAMES

这些 enum 选项具有自描述的名称,并在下一节中解释。

RETAIN_CLASS_REFERENCE

如果我们将第一个选项 enum 常量 RETAIN_CLASS_REFERENCE, 作为 getInstance() 方法的参数,那么返回的实例将使我们能够访问在遍历过程中各个栈帧所引用的类。

SHOW_REFLECT_FRAMES

SHOW_REFLECT_FRAMES enum 常量将生成一个包括来自某些反射调用的帧的遍历器。

SHOW_HIDDEN_FRAMES

最后,枚举常量选项 SHOW_HIDDEN_FRAMES 将包括所有隐藏帧,这些帧包含反射调用以及为 lambda 函数调用生成的调用帧。

下面是一个关于反射和隐藏帧的简单演示:

    package packt; 
    import static java.lang.StackWalker.Option.SHOW_HIDDEN_FRAMES; 
    import static java.lang.StackWalker.Option.SHOW_REFLECT_FRAMES; 
    public class Main { 

允许我们直接执行此代码的 main() 方法调用 simpleCall() 方法:

    public static void main(String[] args) { 
      simpleCall(); 
    } 

方法 simpleCall() 如其名所示,只是简单地调用:

    static void simpleCall() { 
      reflectCall(); 
    } 

链中的下一个方法稍微复杂一些。尽管这也只是调用下一个方法,但它使用反射来做到这一点:

    static void reflectCall() { 
      try { 
        Main.class.getDeclaredMethod("lambdaCall", 
          new Class[0]) 
           .invoke(null, new Object[0]); 
      } catch (Exception e) { 
          throw new RuntimeException(); 
      } 
    } 

在下一个示例中,我们有一个使用 lambda 调用的方法:

    static void lambdaCall() { 
      Runnable r = () -> { 
        walk(); 
      }; 
      r.run(); 
    } 

实际行走前的最后一种方法被称为 walk():

    static void walk() { 
      noOptions(); 
      System.out.println(); 
      reflect(); 
      System.out.println(); 
      hidden(); 
    } 

前面的 walk() 方法依次调用三个方法。这些方法非常相似,这里提供它们:

    static void noOptions() { 
      StackWalker 
        .getInstance() 
        .forEach(System.out::println); 
    } 

    static void reflect() { 
      StackWalker 
        .getInstance(SHOW_REFLECT_FRAMES) 
        .forEach(System.out::println); 
    } 

    static void hidden() { 
      StackWalker 
        // shows also reflect frames 
        .getInstance(SHOW_HIDDEN_FRAMES) 
        .forEach(System.out::println); 
    } 

前面的三个方法将帧打印到标准输出。它们使用堆栈跟踪器的 forEach() 方法。以下是堆栈跟踪程序输出:

stackwalker/packt.Main.noOptions(Main.java:45) 
stackwalker/packt.Main.walk(Main.java:34) 
stackwalker/packt.Main.lambda$lambdaCall$0(Main.java:28) 
stackwalker/packt.Main.lambdaCall(Main.java:30) 
stackwalker/packt.Main.reflectCall(Main.java:19) 
stackwalker/packt.Main.simpleCall(Main.java:12) 
stackwalker/packt.Main.main(Main.java:8) 

此输出仅包含属于我们代码中调用的帧。main() 方法调用 simpleCall(),它又调用 reflectCall(),然后 reflectCall() 调用 lambdaCall()lambdaCall() 调用一个 lambda 表达式,该表达式调用 walk(),依此类推。我们没有指定任何选项并不意味着 lambda 调用会从堆栈中删除。我们执行了那个调用,因此它必须在那里。它删除的是 JVM 实现 lambda 所需要的额外堆栈帧。我们可以在下一个输出中看到,当选项是 SHOW_REFLECT_FRAMES 时,反射帧已经存在:

stackwalker/packt.Main.reflect(Main.java:58) 
stackwalker/packt.Main.walk(Main.java:36) 
stackwalker/packt.Main.lambda$lambdaCall$0(Main.java:28) 
stackwalker/packt.Main.lambdaCall(Main.java:30) 
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 
java.base/java.lang.reflect.Method.invoke(Method.java:547) 
stackwalker/packt.Main.reflectCall(Main.java:19) 
stackwalker/packt.Main.simpleCall(Main.java:12) 
stackwalker/packt.Main.main(Main.java:8) 

在这种情况下,区别在于我们可以看到 reflectCall() 方法调用 lambdaCall() 方法不是直接的。reflectCall() 方法调用 invoke() 方法,它又调用另一个在另一个类中定义的同名方法,然后该方法调用 JVM 提供的本地方法 invoke0()。之后我们最终到达 lambdaCall() 方法。

在输出中我们还可以看到,这些反射调用属于 java.base 模块,而不是我们的 stackwalker 模块。

如果我们除了反射帧外还包含隐藏帧,指定选项 SHOW_HIDDEN_FRAMES,那么我们将看到以下输出:

stackwalker/packt.Main.hidden(Main.java:52) 
stackwalker/packt.Main.walk(Main.java:38) 
stackwalker/packt.Main.lambda$lambdaCall$0(Main.java:28) 
stackwalker/packt.Main$$Lambda$46/269468037.run(Unknown Source) 
stackwalker/packt.Main.lambdaCall(Main.java:30) 
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 
java.base/java.lang.reflect.Method.invoke(Method.java:547) 
stackwalker/packt.Main.reflectCall(Main.java:19) 
stackwalker/packt.Main.simpleCall(Main.java:12) 
stackwalker/packt.Main.main(Main.java:8) 

这包括 JVM 用于执行 lambda 调用的额外隐藏帧。此外,还包括反射帧。

关于枚举常量的最后思考

我们还可以指定多个选项,给出一个选项集。最简单的方法是使用 java.util.Set 接口的静态 of() 方法。这样,RETAIN_CLASS_REFERENCE 选项可以与 SHOW_REFLECT_FRAMES 选项或 SHOW_HIDDEN_FRAMES 选项组合。

虽然技术上可以将 SHOW_REFLECT_FRAMESSHOW_HIDDEN_FRAMES 作为选项集组合,但这样做实际上并没有任何优势。后者包括前者,所以两者的组合与后者完全相同。

访问类

当我们想在堆栈遍历期间访问类对象时,我们必须指定RETAIN_CLASS_REFERENCE选项。尽管StackFrame接口定义了getClassName()方法,可以使用它通过Class.forName()方法访问具有该名称的类,但这样做并不能保证StackFrame对象引用的类是由与调用Class.forName()的代码相同的类加载器加载的。在某些特殊情况下,我们可能会遇到两个不同类加载器加载的两个具有相同名称的不同类。

如果在创建StackWalker实例时没有使用选项,则其他返回类对象的方法将抛出UnsupportedOperationException异常。这样,就不能在StackFrame上使用getDeclaringClass(),在StackWalker上使用getCallerClass()

步骤方法

StackWalker定义了forEach()方法,该方法期望一个Consumer(最好是 lambda 表达式形式),它会对堆栈跟踪中的每个元素进行调用。Consumer方法的参数是一个StackFrame对象。

虽然Stream接口也定义了一个名为forEach的方法,并且walk()方法将一个Stream对象传递给作为参数的Function,但我们不应混淆这两个方法。StackWalkerforEach()方法是一个更简单、大多数情况下效果较差的方法,用于遍历堆栈跟踪的所有元素。

在大多数情况下,这种方法效果较差,因为它迫使StackWalker实例获取堆栈跟踪的所有元素,以便forEach()方法可以遍历每个元素到末尾。如果我们知道我们不会遍历堆栈跟踪到末尾,我们应该使用walk()方法,它以懒惰的方式访问堆栈,从而为性能优化留出更多空间。

StackWalker类有一个walk()方法,这是定义其作为遍历者的方法。该方法接受一个由StackWalker调用的Functionwalk()方法的返回值将是Function返回的对象。Function的参数是一个Stream<StackFrame>,它传递堆栈帧。第一个帧是包含walk()方法调用的帧,下一个是调用walk()方法的调用方法的帧,依此类推。

可以使用Function根据从流中来的StackFrame对象计算一些值,并决定调用者是否有资格调用我们的代码。

在回顾了需要Function作为参数的walk()方法后,你可能会想,为什么它如此复杂。我们可能希望我们能直接从StackWalter实例中获取Stream<StackFrame>。最简单的方法是将流从Function返回。考虑以下示例:

    // EXAMPLE OF WHAT NOT TO DO!!!! 
    public static void itIsNotCallBack() { 
      Stream<StackWalker.StackFrame> stream = 
        StackWalker 
          .getInstance(RETAIN_CLASS_REFERENCE) 
          .walk(s -> s); 
      boolean eligible = // YOU GET EXCEPTION!!!! 
        stream.dropWhile(CheckEligibility::inLibrary) 
          .dropWhile(CheckEligibility::notInLibrary) 
          .count() == 0; 
      if (!eligible) { 
        throw new IllegalCallerException(); 
      } 
    } 

我们所做的是简单地从 walker 调用中直接返回流,并在之后遍历它执行相同的计算。我们的结果是返回一个IllegalStateException异常,而不是执行资格检查。

原因在于StackWalker的实现高度优化。它不会复制整个堆栈以提供流源信息。它从实际的、活动的堆栈工作。为了做到这一点,它必须确保在流使用期间堆栈不会被修改。这与我们在迭代集合时更改集合可能会得到的ConcurrentModificationException非常相似。如果我们把流传递到调用栈中,然后想要从中获取StackFrame,由于我们从属于它的方法返回了,流将尝试从已经消失的堆栈帧中获取信息。这样,StackWalker不会对整个堆栈进行快照,而是从实际的堆栈工作,并且必须确保它需要的堆栈部分不会改变。我们可能从Function中调用方法,这样我们可以在调用链中深入挖掘,但在流使用期间我们不能向上移动。

也不要尝试玩其他技巧,比如扩展StackWalker类。你不能。它是一个final类。

StackFrame

在前面的章节中,我们遍历了StackFrame元素,并提供了示例代码片段,但没有花时间更仔细地检查它。StackFrame是定义在StackWalker类内部的一个接口。它定义了访问器,以及一个可以将信息转换为StackTraceElement的转换器。

接口定义的访问器如下:

  • getClassName()将返回由StackFrame表示的方法的类的二进制名称。

  • getMethodName()将返回由StackFrame表示的方法名。

  • getDeclaringClass()将返回由StackFrame表示的方法的类。如果在创建StackWalker实例时没有使用Option.RETAIN_CLASS_REFERENCE,则该方法将抛出UnsupportedOperationException

  • getByteCodeIndex()获取包含由StackFrame表示的方法执行点的代码数组的索引。在查找由命令行工具javap提供的反汇编 Java 代码时,使用此值可能有助于调试。此值的程序化使用仅对具有直接访问代码字节码的应用程序有价值,例如 java 代理或运行时生成字节码的库。如果方法是本地的,该方法将返回一个负数。

  • getFileName()返回由StackFrame表示的方法定义的源文件名。

  • getLineNumber()返回源代码的行号。

  • isNativeMethod()如果StackFrame表示的方法是本地的,则返回true,否则返回false

StackFrame不提供任何访问方法所属对象的方法。你不能访问由StackFrame表示的方法的参数和局部变量,也没有其他方法可以完成这个任务。这是很重要的。这样的访问会过于侵入性,并且是不可能的。

性能

如果不查看性能考虑因素,我们对StackWalker的覆盖就不会完整。

StackWalker高度优化,不会创建大量未使用的内存结构。这就是为什么我们必须使用传递给walker()方法作为参数的Function的原因。这也是为什么StackTrace在创建时不会自动转换为StackTraceElement的原因。这只有在查询特定StackTraceElement的方法名和行号时才会发生。理解这一点很重要,因为这个转换需要相当多的时间,如果它被用于代码中的某些调试目的,那么它不应该被留下。

为了使StackWalker更快,我们可以提供一个关于我们将要处理的StackFrame元素数量的估计。如果我们不提供这样的估计,JDK 当前实现将使用预先分配的八个StackFrame对象,当这些用完时,JDK 将分配更多的。JDK 将根据我们的估计分配元素的数量,除非我们估计的值大于 256。在这种情况下,JDK 将使用 256。

摘要

在本章中,我们学习了如何使用StackWalker并提供了示例代码。我们对 API 的详细审查包括了不同的使用场景、选项和信息。我们解释了 API 的复杂性,并分享了如何以及如何不使用该类。我们以一些用户必须注意的相关性能问题结束。

在我们接下来的章节中,我们将涵盖十几个被纳入 Java 9 平台的 Java 增强提案。这些重点变更将覆盖广泛的工具和 API 更新,旨在使使用 Java 开发更加容易,并能够创建优化的 Java 应用程序。我们将探讨新的 HTTP 客户端、Javadoc 和 Doclet API 的变更、新的 JavaScript 解析器、JAR 和 JRE 的变更、新的 Java 级别 JVM 编译器接口、对 TIFF 图像的支持、平台日志记录、XML 目录支持、集合、新的平台特定桌面功能,以及方法处理和弃用注解的增强。

第十一章:新工具和工具增强

在上一章中,我们探讨了 Java 9 的新堆栈跟踪器 API,并学习了它如何使 Java 应用程序能够遍历调用堆栈。这是一个在 Java 应用程序中不常实现的专业功能。话虽如此,该 API 可能适用于一些非常特殊的情况,例如由框架提供的功能。您了解到,如果您开发支持框架的应用程序编程,并且需要依赖于调用上下文的代码,那么堆栈跟踪器 API 就是您所需要的。我们还发现,该 API 提供了对调用堆栈的快速和优化访问,实现了对单个帧的懒访问。

在本章中,我们将介绍被纳入 Java 9 平台的 16 个Java 增强提案JEPs)。这些 JEPs 涵盖了广泛的工具和 API 更新,以使使用 Java 开发更加容易,并为我们的程序提供更大的优化可能性。

我们对新的工具和工具增强的回顾将包括以下内容:

  • 新的 HTTP 客户端

  • Javadoc 和 Doclet API

  • mJRE 更改

  • JavaScript 解析器

  • 多版本 JAR 文件

  • Java 级别的 JVM 编译器接口

  • TIFF 支持

  • 平台日志记录

  • XML 目录

  • 集合

  • 平台特定的桌面功能

  • 增强方法处理

  • 增强弃用

新的 HTTP 客户端[JEP-110]

在本节中,我们将回顾 Java 的超文本传输协议HTTP)客户端,从 Java 9 之前的样式开始,然后深入探讨 Java 9 平台中的新 HTTP 客户端。这种做法是为了支持对 Java 9 中做出的更改的理解。

Java 9 之前的 HTTP 客户端

JDK 版本 1.1 引入了支持 HTTP 特定功能的HttpURLConnection API。这是一个包含此处列出的字段的健壮类:

  • chunkLength

  • fixedContentLength

  • fixedContentLengthLong

  • HTTP_ACCEPTED

  • HTTP_BAD_GATEWAY

  • HTTP_BAD_METHOD

  • HTTP_BAD_REQUEST

  • HTTP_CLIENT_TIMEOUT

  • HTTP_CONFLICT

  • HTTP_CREATED

  • HTTP_ENTITY_TOO_LARGE

  • HTTP_FORBIDDEN

  • HTTP_GONE

  • HTTP_INTERNAL_ERROR

  • HTTP_LENGTH_REQUIRED

  • HTTP_MOVED_PERM

  • HTTP_MOVED_TEMP

  • HTTP_MULT_CHOICE

  • HTTP_NO_CONTENT

  • HTTP_NOT_ACCEPTABLE

  • HTTP_NOT_AUTHORITATIVE

  • HTTP_NOT_FOUND

  • HTTP_NOT_IMPLEMENTED

  • HTTP_NOT_MODIFIED

  • HTTP_OK

  • HTTP_PARTIAL

  • HTTP_PAYMENT_REQUIRED

  • HTTP_PRECON_FAILED

  • HTTP_PROXY_AUTH

  • HTTP_REQ_TOO_LONG

  • HTTP_RESET

  • HTTP_SEE_OTHER

  • HTTP_SERVER_ERROR

  • HTTP_UNAUTHORIZED

  • HTTP_UNAVAIABLE

  • HTTP_UNSUPPORTED_TYPE

  • HTTP_USE_PROXY

  • HTTP_VERSION

  • instanceFollowRedirects

  • method

  • responseCode

  • responseMessage

如您从字段列表中看到的,对 HTTP 的支持非常广泛。除了构造函数之外,还有许多可用的方法,包括以下这些:

  • disconnect()

  • getErrorStream()

  • getFollowRedirects()

  • getHeaderField(int n)

  • getHeaderFieldDate(String name, long Default)

  • getHeaderFieldKey(int n)

  • getInstanceFollowRedirects()

  • getPermission()

  • getRequestMethod()

  • getResponseCode()

  • getResponseMessage()

  • setChunkedStreamingMode(int chunklen)

  • setFixedLengthStreamingMode(int contentLength)

  • setFixedlengthStreamingMode(long contentLength)

  • setFollowRedirects(boolean set)

  • setInstanceFollowRedircts(boolean followRedirects)

  • setRequestMethod(String method)

  • usingProxy()

之前列出的类方法除了从java.net.URLConnection类和java.lang.Object类继承的方法外,还包括。

原始 HTTP 客户端存在一些问题,这使得它非常适合使用新的 Java 平台进行更新。这些问题如下:

  • 基础URLConnection API 有,如 Gopher 和 FTP 等已废弃的协议,随着时间的推移越来越多

  • HttpURLConnection API 在 HTTP 1.1 之前就已经存在,并且过于抽象,使其不太可用

  • HTTP 客户端严重缺乏文档,使得 API 令人沮丧且难以使用

  • 客户端一次只能在一个线程上运行

  • 由于它早于 HTTP 1.1 并且缺乏足够的文档,该 API 难以维护

既然我们已经知道了 HTTP 客户端的问题,让我们看看 Java 9 有哪些期待。

Java 9 的新 HTTP 客户端

创建 Java 9 平台的新的 HTTP 客户端有几个相关目标。JEP-110 是新 HTTP 客户端的组织提案。JEP-110 的主要目标在此列出,并展示了所提出的新 HTTP 客户端。这些目标按易用性、核心功能、其他功能和性能的广泛类别呈现:

  • 易用性:

    • 该 API 旨在提供高达 90%的 HTTP 相关应用程序需求。

    • 新的 API 简单易用,适用于最常见的用例。

    • 包含一个简单的阻塞模式。

    • 该 API 支持现代 Java 语言特性。Java 8 中引入的主要新特性 Lambda 表达式就是一个例子。

  • 核心功能:

    • 支持 HTTPS/TLS

    • 支持 HTTP/2

    • 提供有关 HTTP 协议请求和响应的所有细节的可见性

    • 支持标准/常用认证机制

    • 提供接收头部事件通知

    • 提供接收响应体事件通知

    • 提供错误事件通知

  • 其他功能:

    • 新的 API 可用于 WebSocket 握手

    • 它与当前的网络 API 一起执行安全检查

  • 性能:

    • 对于 HTTP/1.1:

      • 新的 API 必须至少与之前的 API 一样高效。

      • 当用作客户端 API 时,内存消耗不得超过 Apache HttpClient、Netty 和 Jetty。

    • 对于 HTTP/2:

      • 性能必须超过 HTTP/1.1。

      • 当用作客户端 API 时,新的性能必须与 Netty 和 Jetty 相当或更好。新的客户端不应导致性能下降。

      • 当用作客户端 API 时,内存消耗不得超过 Apache HttpClient、Netty 和 Jetty。

    • 避免运行计时线程

新 API 的限制

新 API 有一些故意的不足之处。虽然这听起来可能有些反直觉,但新的 API 并不是旨在完全取代当前的HttpURLConnection API。相反,新的 API 旨在最终取代当前的 API。

以下代码片段提供了一个示例,说明如何在 Java 应用程序中实现HttpURLConnect类以打开和读取 URL:

    /*
    import statements
    */

    public class HttpUrlConnectionExample
    {  
      public static void main(String[] args) 
      {
        new HttpUrlConnectionExample();
      }

      public HttpUrlConnectionExample()
      {
        URL theUrl = null;
        BufferedReader theReader = null;
        StringBuilder theStringBuilder;

        // put the URL into a String
        String theUrl = "https://www.packtpub.com/";

        // here we are creating the connection
        theUrl = new URL(theUrl);
        HttpURLConnection theConnection = (HttpURLConnection) 
         theUrl.openConnection();

        theConnection.setRequestedMethod("GET");

        // add a delay 
        theConnection.setReadTimeout(30000); // 30 seconds
        theConnection.connect();

        // next, we can read the output
        theReader = new BufferedReader(
          new InputStreamReader(theConnection.getInputStream()));
        theStringBuilder =  new StringBuilder();

        // read the output one line at a time
        String theLine = null;
        while ((theLine = theReader.readLine() != null)
        {
          theStringBUilder.append(line + "\n");
        }

        // echo the output to the screen console
        System.out.println(theStringBuilder.toString());

        // close the reader
        theReader.close();
      }
    }
    . . . 

为了简洁,前面的代码没有包含异常处理。

新 API 的一些具体限制如下:

  • 并非所有与 HTTP 相关的功能都得到支持。据估计,大约有 10%的 HTTP 协议没有被 API 暴露。

  • 标准的/常见的身份验证机制仅限于基本身份验证。

  • 新 API 的总体目标是使用简单性,这意味着性能改进可能不会实现。当然,不会有性能下降,但也不太可能有一个压倒性的改进水平。

  • 对请求的过滤没有支持。

  • 对响应的过滤没有支持。

  • 新的 API 不包括可插拔的连接缓存。

  • 缺乏一个通用的升级机制。

新 API 作为 Java 9 平台的一部分以孵化模式提供。这表明该 API 将在未来的 Java 平台上标准化,可能是 Java 10。

简化的 Doclet API [JEP-221]

Doclet API 和 Javadoc 密切相关。Javadoc 是一个文档工具,而 Doclet API 提供了功能,使我们能够检查嵌入在库和程序源代码级别的 javadoc 注释。在本节中,我们将回顾 Java 9 之前的 Doclet API 状态,然后探讨 Java 9 平台引入的 Doclet API 的变化。在下一节中,我们将回顾 Javadoc。

Java 9 之前的 Doclet API

Java 9 之前的 Doclet API,或com.sun.javadoc包,使我们能够查看位于源代码中的 javadoc 注释。通过使用start方法调用 Doclet。该方法签名是public static boolean start(RootDoc root)。我们将使用RootDoc实例作为程序结构信息的容器。

为了调用 javadoc,我们需要传递以下内容:

  • 包名

  • 源文件名(对于类和接口)

  • 一个访问控制选项——以下之一:

    • package

    • private

    • protected

    • public

当使用前面列出的项目调用 javadoc 时,会提供一个文档集作为过滤后的列表。如果我们目的是获得一个全面、未过滤的列表,我们可以使用allClasses(false)

让我们回顾一个示例 Doclet:

    // Mandatory import statement.
    import com.sun.javadoc.*;

    // We will be looking for all the @throws documentation tags.
    public class AllThrowsTags extends Doclet 
    {
      // This is used to invoke the Doclet.
      public static boolean start(Rootdoc myRoot) 
      {
        // "ClassDoc[]" here referes to classes and interfaces.
        ClassDoc[] classesAndInterfaces = 
         myRoot.classesAndInterfaces();
        for (int i = 0; i < classesAndInterfaces.length; ++i)
        {
          ClassDoc tempCD = classesAndInterfaces[i];
          printThrows(tempCD.contructors());
          printThrows(tempCD.methods());
        }
        return true;
      }

      static void printThrows(ExecutableMemberDoc[] theThrows)
      {
        for (int i = 0; i < theThrows.length; ++i)
        {
          ThrowsTag[] throws = theThrows[i].throwsTags();

          // Print the "qualified name" which will be a the
             class or 
          // interface name.
          System.out.println(theThrows[i].qualifiedName());

          // A loop to print all comments with the Throws Tag that 
          // belongs to the previously printed class or
             interface name
          for (int j = 0; j < throws.length; ++j)
          {
            // A println statement that calls three methods
               from the 
            // ThrowsTag Interface: exceptionType(),
               exceptionName(),
            // and exceptionComment().
            System.out.println("--> TYPE: " +
              throws[j].exceptionType() + 
              " | NAME: " + throws[j].exceptionName() + 
              " | COMMENT: " + throws[j].exceptionComment());
          }
        }
      }
    }

如您通过详尽的注释代码所看到的,获取 javadoc 内容相对容易。在我们的前一个示例中,我们将在命令行中使用以下代码调用AllThrows类:

javadoc -doclet AllThrowsTags -sourcepath <source-location> java.util

我们的结果输出将包括以下结构:

<class or interface name>
 TYPE: <exception type> | NAME: <exception name> | COMMENT: <exception comment>
 TYPE: <exception type> | NAME: <exception name> | COMMENT: <exception comment>
 TYPE: <exception type> | NAME: <exception name> | COMMENT: <exception comment>
<class or interface name>
 TYPE: <exception type> | NAME: <exception name> | COMMENT: <exception comment>
 TYPE: <exception type> | NAME: <exception name> | COMMENT: <exception comment>

API 枚举

该 API 包含一个枚举,LanguageVersion,它提供了 Java 编程语言的版本。该枚举的常量有Java_1_1Java_1_5

API 类

Doclet类提供了一个如何创建一个类以启动 Doclet 的示例。它包含一个空的Doclet()构造函数和以下方法:

  • languageVersion()

  • optionLength(String option)

  • start(RootDoc root)

  • validOptions(String[][] options, DocErrorReporter reporter)

API 接口

Doclet API 包含以下列出的接口。接口名称是自我描述的。你可以查阅文档以获取更多详细信息:

  • AnnotatedType

  • AnnotationDesc

  • AnnotationDesc.ElementValuePair

  • AnnotationTypeDoc

  • AnnotationTypeElementDoc

  • AnnotationValue

  • ClassDoc

  • ConstructorDoc

  • Doc

  • DocErrorReporter

  • ExecutableMemberDoc

  • FieldDoc

  • MemberDoc

  • MethodDoc

  • PackageDoc

  • Parameter

  • ParameterizedType

  • ParamTag

  • ProgramElementDoc

  • RootDoc

  • SeeTag

  • SerialFieldTag

  • SourcePosition

  • Tag

  • ThrowsTag

  • Type

  • TypeVariable

  • WildcardType

存在的 Doclet API 的问题

驱动需要新的 Doclet API 的是几个与现有 Doclet API 相关的问题:

  • 它不适合测试或并发使用。这源于其静态方法的实现。

  • API 中使用的语言模型存在一些限制,并且随着每个后续的 Java 升级而变得更加有问题。

  • 该 API 效率低下,很大程度上是由于其大量使用子字符串匹配。

  • 没有提供有关任何给定注释具体位置的参考。这使得诊断和故障排除变得困难。

Java 9 的 Doclet API

现在你已经很好地掌握了 Java 9 之前存在的 Doclet API,让我们看看 Java 9 平台所做出的更改和交付的内容。新的 Doclet API 位于jdk.javadoc.doclet包中。

从高层次来看,Doclet API 的更改如下:

  • 更新com.sun.javadoc Doclet API 以利用几个 Java SE 和 JDK API

  • 更新com.sun.tools.doclets.standard.Standard Doclet 以使用新的 API

  • 支持更新的 Taglet API,该 API 用于创建自定义 javadoc 标签

除了前面列出的更改之外,新的 API 还使用了这里列出的两个 API:

  • 编译器树 API

  • 语言模型 API

让我们在接下来的部分中探讨这些内容。

编译器树 API

编译器树 API 位于com.sun.source.doctree包中。它提供了几个接口来文档化源级注释。这些 API 表示为抽象语法树ASTs)。

有两个枚举:

  • AttributeTree.ValueKind具有以下常量:

    • DOUBLE

    • EMPTY

    • SINGLE

    • UNQUOTED

  • DocTree.Kind具有以下常量:

    • ATTRIBUTE

    • AUTHOR

    • CODE

    • COMMENT

    • DEPRECATED

    • DOC_COMMENT

    • DOC_ROOT

    • END_ELEMENT

    • ENTITY

    • ERRONEOUS

    • EXCEPTION

    • IDENTIFIER

    • INHERIT_DOC

    • LINK

    • LINK_PLAIN

    • LITERAL

    • OTHER

    • PARAM

    • REFERENCE

    • RETURN

    • SEE

    • SERIAL

    • SERIAL_DATA

    • SERIAL_FIELD

    • SINCE

    • START_ELEMENT

    • TEXT

    • THROWS

    • UNKNOWN_BLOCK_TAG

    • UNKNOWN_INLINE_TAG

    • VALUE

    • VERSION

com.sun.source.doctree包包含几个接口。它们在以下表格中详细说明:

接口名称 扩展 树节点用于: 非继承方法
AttributeTree DocTree HTML 元素 getName(), getValue(), getValueKind()
AuthorTree BlockTagTree, DocTree @author块标签 getName()
BlockTagTree DocTree 不同类型块标签的基类 getTagName()
CommentTree DocTree 包含以下 HTML 标签的嵌入式 HTML 注释--<!--text--> getBody()
DeprecatedTree BlockTagTree @deprecated块标签 getBody()
DocCommentTree DocTree 主体块标签 getBlockTags(), getBody(), getFirstSentence()
DocRootTree InlineTagTree @docroot内联标签 N/A
DocTree N/A 所有接口的通用接口 accept(DocTreeVisitor<R,D>visitor,Ddata), getKind()
DocTreeVisitor<R,P> N/A R = 访问者方法的返回类型; P = 额外参数的类型 visitAttribute(AttributeTree node, P p), visitAuthor(AuthorTree node, P p), visitComment(CommentTree node, P p), visitDeprecated(DeprecatedTree node, P p), visitDocComment(DocCommentTree node, P p), visitDocRoot(DocRootTree node, P p), visitEndElement(EndElementTree node, P p), visitEntity(EntityTree node, P p), visitErroneous(ErroneousTree node, P p), visitIdentifier(IdentifierTree node, P p), visitInheritDoc(InheritDocTree node, P p), visitLink(LinkTree node, P p), visitLiteral(LiteralTree node, P p), visitOther(DocTree node, P p), visitParam(ParamTree node, P p), visitReference(ReferenceTree node, P p), visitReturn(ReturnTree node, P p), visitSee(SeeTree node, P p), visitSerial(SerialTree node, P p), visitSerialData(SerialDataTree node, P p), visitSerialField(SerialFieldTree node, P p), visitSince(SinceTree node, P p), visitStartElement(StartElementTree node, P p), visitText(TextTree node, P p), visitThrows(ThrowsTree node, P p), visitUnknownBlockTag(UnknownBlockTagTree node, P p), visitUnknownInlineTag(UnknownInlineTagTree node, P p), visitValue(ValueTree node, P p), visitVersion(VersionTree node, P p)
EndElementTree DocTree HTML 元素的结束</name> getName()
EntityTree DocTree HTML 实体 getName()
ErroneousTree TextTree 用于格式错误的文本 getDiagnostic()
IdentifierTree DocTree 注释中的标识符 getName()
InheritDocTree InlineTagTree @inheritDoc内联标签 N/A
InlineTagTree DocTree 内联标签的通用接口 getTagName()
LinkTree InlineTagTree @link@linkplan内联标签 getLabel(), getReference()
LiteralTree InlineTagTree @literal@code内联标签 getBody()
ParamTree BlockTagTree @param块标签 getDescription(), getName(), isTypeParameter()
ReferenceTree DocTree 用于引用 Java 语言元素 getSignature()
ReturnTree BlockTagTree @return块标签 getDescription()
SeeTree BlockTagTree @see块标签 getReference()
SerialDataTree BlockTagTree @serialData块标签 getDescription()
SerialFieldTree BlockTagTree @serialData块标签和@serialField字段名及描述 getDescription(), getName(), getType()
SerialTree BlockTagTree @serial块标签 getDescription()
SinceTree BlockTagTree @since块标签 getBody()
StartElementTree DocTree HTML 元素的开始 < name [attributes] [/] > getAttributes(), getName(), isSelfClosing()
TextTree DocTree 纯文本 getBody()
ThrowsTree BlockTagTree @exception@throws块标签 getDescription(), getExceptionname()
UnknownBlockTagTree BlockTagTree 不可识别的内联标签 getContent()
UnknownInlineTagTree InlineTagTree 不可识别的内联标签 getContent()
ValueTree InlineTagTree @value内联标签 getReference()
VersionTree BlockTagTree @version块标签 getBody()

语言模型 API

语言模型 API 位于java.lang.model包中。它包括用于语言处理和语言建模的包和类。它由以下组件组成:

  • AnnotatedConstruct 接口

  • SourceVersion 枚举

  • UnknownEntityException 异常

在接下来的三个部分中将进一步探讨这些语言模型 API 组件。

AnnotatedConstruct 接口

AnnotatedConstruction 接口为语言模型 API 提供了一个可注解的构造,该 API 自 Java 平台 1.8 版本以来一直是 Java 平台的一部分。它适用于元素(接口Element)或类型(接口TypeMirror)的构造。这些构造的注解各不相同,如表中所示:

构造类型 接口 注解
element Element 声明
type TypeMirror 基于类型名称的使用

AnnotatedConstruction 接口有三个方法:

  • getAnnotation(Class<A> annotationType): 此方法返回构造注解的类型

  • getAnnotationMirrors(): 此方法返回构造上的注解列表

  • getAnnotationsByType(Class<A> annotationType): 此方法返回构造关联的注解

SourceVersion 枚举

SourceVersion 枚举包含以下常量:

  • RELEASE_0

  • RELEASE_1

  • RELEASE_2

  • RELEASE_3

  • RELEASE_4

  • RELEASE_5

  • RELEASE_6

  • RELEASE_7

  • RELEASE_8

预计一旦 Java 9 平台正式发布,SourceVersion枚举将更新以包括RELEASE_9

此枚举还包含几个方法,如下所示:

方法名称: isIdentifier

public static boolean isIdentifier(CharSequence name)

此方法返回 true 如果参数字符串是 Java 标识符或关键字。

方法名称: isKeyword

public static boolean isKeyword(CharSequence s)

此方法返回 true 如果给定的 CharSequence 是一个字面量或关键字。

方法名称: isName

public static boolean isName(CharSequence name)

此方法返回 true 如果 CharSequence 是一个有效的名称。

方法名称: latest

public static SourceVersion latest()

此方法返回建模目的的最新源版本。

方法名称: latestSupported

public static SourceVersion latestSupported()

此方法返回可以完全支持的最新源版本。

方法名称: valueOf

public static SourceVersion valueOf(String name)

此方法根据提供的参数字符串返回枚举常量。

您应该知道,value(String name) 方法会抛出两个异常:IllegalArgumentExceptionNullPointerException

方法名称: values

public static SourceVersion[] values()

此方法返回枚举常量的数组。

未知实体异常

UnknownEntityException 类扩展了 RuntimeException 并是未知异常的超类。类的构造函数如下:

    protected UnknownEntityException(String message)

构造函数使用提供的字符串参数创建一个新的 UnknownEntityException 实例。该方法不接受额外的参数。

此类没有自己的方法,但如所示从 java.lang.Throwableclass.java.lang.Object 类继承方法:

java.lang.Throwable 类的方法:

  • addSuppressed()

  • fillInStackTrace()

  • getCause()

  • getLocalizedMessage()

  • getMessage()

  • getStackTrace()

  • getSuppressed()

  • initCause()

  • printStackTrace()

  • setStackTrace()

  • toString()

java.lang.Object 类的方法:

  • clone()

  • equals()

  • finalize()

  • getClass()

  • hashCode()

  • notify()

  • notifyAll()

  • wait()

HTML5 Javadoc [JEP-224]

Javadoc 工具已更新以支持 Java 9 平台。它现在可以生成 HTML 5 标记输出,除了 HTML 4。新的 Javadoc 工具提供了对 HTML 4 和 HTML 5 的支持。

即使 Java 9 平台出现,HTML 4 也将继续作为默认的 Javadoc 输出格式。HTML 5 将是一个选项,并且不会在 Java 10 之前成为默认输出标记格式。

以下简短的 Java 应用程序简单地生成一个宽 319、高 319 的框架。这里显示时没有使用任何 Javadoc 标签,我们将在本节稍后讨论:

    /import javax.swing.JFrame;
    import javax.swing.WindowConstants;

    public class JavadocExample 
    {

      public static void main(String[] args) 
      {
        drawJFrame();
      }

      public static void drawJFrame()
      {
        JFrame myFrame = new JFrame("Javadoc Example");
        myFrame.setSize(319,319);
        myFrame.setDefaultCloseOperation(
          WindowConstants.EXIT_ON_CLOSE);
        myFrame.setVisible(true);
      }
    }

当您的包或类完成时,您可以使用 Javadoc 工具生成 Javadoc。您可以从命令行或从您的 集成开发环境IDE)中运行位于 JDK /bin 目录中的 Javadoc 工具。每个 IDE 处理 Javadoc 生成的方式不同。例如,在 Eclipse 中,您会从下拉菜单中选择项目,然后生成 Javadoc。在 IntelliJ IDEA IDE 中,您会选择工具下拉菜单,然后生成 Javadoc。

下面的截图显示了 IntelliJ IDEA 中生成 Javadoc 功能的界面。如您所见,已包含 -html5 命令行参数:

当您点击“确定”按钮时,您将看到一系列状态消息,如下例所示:

"C:\Program Files\Java\jdk-9\bin\javadoc.exe" -public -splitindex -use -author -version -nodeprecated -html5 @C:\Users\elavi\AppData\Local\Temp\javadoc1304args.txt -d C:\Chapter11\JD-Output
Loading source file C:\Chapter11\src\JavadocExample.java...
Constructing Javadoc information...
Standard Doclet version 9
Building tree for all the packages and classes...
Generating C:\Chapter11\JD-Output\JavadocExample.html...
Generating C:\Chapter11\JD-Output\package-frame.html...
Generating C:\Chapter11\JD-Output\package-summary.html...
Generating C:\Chapter11\JD-Output\package-tree.html...
Generating C:\Chapter11\JD-Output\constant-values.html...
Generating C:\Chapter11\JD-Output\class-use\JavadocExample.html...
Generating C:\Chapter11\JD-Output\package-use.html...
Building index for all the packages and classes...
Generating C:\Chapter11\JD-Output\overview-tree.html...
Generating C:\Chapter11\JD-Output\index-files\index-1.html...
Generating C:\Chapter11\JD-Output\index-files\index-2.html...
Generating C:\Chapter11\JD-Output\index-files\index-3.html...
Building index for all classes...
Generating C:\Chapter11\JD-Output\allclasses-frame.html...
Generating C:\Chapter11\JD-Output\allclasses-frame.html...
Generating C:\Chapter11\JD-Output\allclasses-noframe.html...
Generating C:\Chapter11\JD-Output\allclasses-noframe.html...
Generating C:\Chapter11\JD-Output\index.html...
Generating C:\Chapter11\JD-Output\help-doc.html...

javadoc exited with exit code 0

当 Javadoc 工具退出后,您就可以查看 Javadoc。以下是根据之前提供的代码生成的截图。如您所见,它以与 Oracle 正式 Java 文档相同的方式格式化:

当我们生成 Javadoc 时,会创建多个文档,如下面的截图所示,展示了提供的目录树:

您还可以添加 Javadoc 工具可识别的可选标签。这些标签在此提供:

  • @author

  • @code

  • @deprecated

  • @docRoot

  • @exception

  • @inheritDoc

  • @link

  • @linkplain

  • @param

  • @return

  • @see

  • @serial

  • @serialData

  • @serialField

  • @since

  • @throws

  • @value

  • @version

关于如何为 Javadoc 工具编写文档注释的更多信息,您可以访问 Oracle 的官方说明www.oracle.com/technetwork/articles/java/index-137868.html

Javadoc 搜索 [JEP-225]

在 Java 9 之前,标准 Doclet 生成的 API 文档页面难以导航。除非您非常熟悉这些文档页面的布局,否则您可能会使用基于浏览器的查找功能来搜索文本。这被认为是不灵活且次优的。

Java 9 平台包括一个搜索框,作为 API 文档的一部分。这个搜索框由标准 Doclet 提供,可以用于在文档中搜索文本。这对开发者来说非常方便,可能会改变我们对 Doclet 生成文档的使用方式。

使用新的 Javadoc 搜索功能,我们可以搜索以下索引组件:

  • 模块名

  • 包名

  • 类型

  • 成员

  • 使用新的 @index 内联标签索引的术语/短语

介绍驼峰式搜索

新的 Javadoc 搜索功能包括一个使用驼峰式搜索的快捷方式。例如,我们可以搜索 openED 来找到 openExternalDatabase() 方法。

移除启动时 JRE 版本选择 [JEP-231]

在 Java 9 之前,我们可以使用 mJRE多个 JRE)功能来指定用于启动我们应用程序的特定 JRE 版本或版本范围。我们会通过命令行选项 -version 或在 JAR 文件清单中的条目来完成此操作。以下流程图说明了基于我们的选择会发生什么:

这个功能是在 JDK 5 中引入的,在那个版本或任何后续版本(直到 JDK 9)中都没有得到充分记录。

以下是在 Java 9 平台上引入的具体更改:

  • mJRE 功能已被移除。

  • 当使用 -version 命令行选项时,启动器现在将产生一个错误。这是一个终止错误,因为处理将不会继续。

  • 如果 JAR 文件的清单中存在 -version 条目,将会产生一个警告。这个警告不会停止执行。

有趣的是,在清单文件中存在 -version 条目只会生成一个警告。这是设计上的考虑,为了考虑到该条目可能存在于较旧的 JAR 文件中的可能性。据估计,当 Java 10 平台发布时,这个警告将被改为一个终止错误。

Nashorn 的解析器 API [JEP-236]

JEP 236 的重点是创建 Nashorn 的 EMCAScript 抽象语法树 API。在本节中,我们将分别查看 Nashorn、EMCAScript 以及解析器 API。

Nashorn

Oracle Nashorn 是 Oracle 开发的用于 JVM 的 JavaScript 引擎,用 Java 编写。它与 Java 8 一起发布。它被创建是为了为开发者提供一个高效且轻量级的 JavaScript 运行时引擎。使用这个引擎,开发者能够将 JavaScript 代码嵌入到他们的 Java 应用程序中。在 Java 8 之前,开发者可以访问由 Netscape 创建的 JavaScript 引擎。这个引擎是在 1997 年引入的,由 Mozilla 维护。

Nashorn 可以用作命令行工具,也可以用作 Java 应用程序中的嵌入式解释器。让我们看看这两种示例。

Nashorn 是德语中犀牛的意思。这个名字来源于 Mozilla 基金会的名为 Rhino 的 JavaScript 引擎。据说 Rhino 的名字来源于 JavaScript 书籍封面上的动物图片。把这个归入 有趣的事实

使用 Nashorn 作为命令行工具

Nashorn 可执行文件 jjs.exe 位于 \bin 文件夹中。要访问它,你可以导航到该文件夹,或者如果你的系统路径设置得当,你可以在系统的终端/命令提示符窗口中输入 jjs 命令来启动壳:

在这里,你可以看到一个打开的终端窗口,它首先检查 Java 的版本,然后使用 jjs -version 命令来启动 Nashorn 壳。在这个例子中,Java 和 Nashorn 都是 1.8.0.121 版本。或者,我们也可以简单地使用 jjs 命令启动 Nashorn,壳将打开而不显示版本标识:

图片

接下来,让我们创建一个简短的 JavaScript 代码,并使用 Nashorn 运行它。考虑以下具有三条简单输出行的简单 JavaScript 代码。

    var addtest = function()
    {
      print("Simple Test");
      print("This JavaScript program adds the numbers 300
       and 19.");
      print("Addition results = " + (300 + 19));
    }
    addtest();

要让 Java 运行此 JavaScript 应用程序,我们将使用jjs address.js命令。以下是输出结果:

图片

使用 Nashorn 有很多事情可以做。从命令提示符/终端窗口,我们可以使用带有-help选项的jjs来查看完整的命令行命令列表:

图片

如您所见,使用-scripting选项使我们能够使用 Nashorn 作为文本编辑器创建脚本。Nashorn 有几个内置函数,在使用 Nashorn 时非常有用:

  • echo(): 这类似于System.out.print() Java 方法

  • exit(): 这将退出 Nashorn

  • load(): 这从给定的路径或 URL 加载脚本

  • print(): 这类似于System.out.print() Java 方法

  • readFull(): 这读取文件的内容

  • readLine(): 这从stdin读取一行

  • quit(): 这将退出 Nashorn

将 Nashorn 用作嵌入式解释器

与将其用作命令行工具相比,Nashorn 的一个更常见用途是将其用作嵌入式解释器。javax.script API 是公开的,可以通过nashorn标识符访问。以下代码演示了如何在 Java 应用程序中访问 Nashorn,定义一个 JavaScript 函数,并获取结果——所有这些都在 Java 应用程序内部完成:

    // required imports
    import javax.script.ScriptEngine;
    import javax.script.ScriptEngineManager;

    public class EmbeddedAddTest 
    {
      public static void main(String[] args) throws Throwable
      {
        // instantiate a new ScriptEngineManager
        ScriptEngineManager myEngineManager =
          new ScriptEngineManager();

        // instantiate a new Nashorn ScriptEngine
        ScriptEngine myEngine = myEngineManager.getEngineByName(
         "nashorn");

        // create the JavaScript function
        myEngine.eval("function addTest(x, y) { return x + y; }");

        // generate output including a call to the addTest function
           via the engine
        System.out.println("The addition results are:
         " + myEngine.eval("addTest(300, 19);"));
      }
    }

这里是控制台窗口中提供的输出:

图片

这是一个简单的例子,旨在让您了解使用嵌入式 Nashorn 可以实现什么。Oracle 的官方文档中有大量示例。

ECMAScript

EMCA欧洲计算机制造商协会)于 1961 年成立,作为信息系统和通信系统的标准组织。今天,EMCA 继续制定标准和发布技术报告,以帮助标准化消费电子、信息系统和通信技术的使用。他们有超过 400 个 ECMA 标准,其中大部分已被采用。

您会注意到 EMCA 不再全部大写,因为它不再被视为缩写。1994 年,欧洲计算机制造商协会正式将其名称更改为 EMCA。

ECMAScript,也称为 ES,于 1997 年作为一个脚本语言规范被创建。JavaScript 实现了这个规范。该规范包括以下内容:

  • 补充技术

  • 脚本语言语法

  • 语义

解析器 API

Java 平台在版本 9 中的一个变化是为 Nashorn 的 ECMAScript 抽象语法树提供特定支持。新 API 的目标是提供以下功能:

  • 表示 Nashorn 语法树节点的接口

  • 能够创建可以配置命令行选项的解析器实例

  • 用于与 AST 节点接口的访问者模式 API

  • 用于使用 API 的测试程序

新的 API,jdk.nashorn.api.tree,被创建以允许对 Nashorn 类进行未来的更改。在新解析器 API 之前,IDE 使用 Nashorn 的内部 AST 表示进行代码分析。根据 Oracle 的说法,使用 idk.nashorn.internal.ir 包阻止了 Nashorn 内部类的现代化。

下面是查看新 jdk.nashorn.api.tree 包的类层次结构:

下面的图形展示了新 API 的复杂性,包括完整的接口层次结构:

jdk.nashorn.api.tree 包的最后一个组件是枚举层次结构,如下所示:

多版本 JAR 文件 [JEP-238]

在 Java 9 平台上扩展了 JAR 文件格式,现在允许单个 JAR 文件中存在多个版本的类文件。类版本可以针对特定的 Java 发布版本。此增强允许开发者使用单个 JAR 文件来存放其软件的多个版本。

JAR 文件增强包括以下内容:

  • 支持 JarFile API

  • 支持标准类加载器

对 JAR 文件格式的更改导致了核心 Java 工具的必要更改,以便它们能够解释新的多版本 JAR 文件。这些核心工具包括以下内容:

  • javac

  • javap

  • jdeps

最后,新的 JAR 文件格式支持模块化作为 Java 9 平台的关键特性。对 JAR 文件格式的更改并未导致相关工具或过程的性能降低。

识别多版本 JAR 文件

多版本 JAR 文件将有一个新的属性,Multi-Release: true。此属性位于 JAR 文件的 MANIFEST.MF 主部分。

标准 JAR 文件和多版本 JAR 文件之间的目录结构将不同。下面是查看典型 JAR 文件结构的情况:

此图展示了新的多版本 JAR 文件结构,其中包含针对 Java 8 和 Java 9 的特定版本类文件:

相关 JDK 变更

为了支持新的多版本 JAR 文件格式,对 JDK 进行了多项更改。这些更改包括以下内容:

  • URLClassLoader 是基于 JAR 的,并被修改成能够读取指定版本的类文件。

  • 新的基于模块的类加载器,Java 9 的新特性,被编写成能够读取指定版本的类文件。

  • java.util.jar.JarFile 类已被修改,以便从多版本 JAR 文件中选择适当的类版本。

  • JAR URL 方案的协议处理程序已被修改,以便从多版本 JAR 文件中选择适当的类版本。

  • Java 编译器javac被修改为读取已识别的类文件版本。这些版本识别是通过使用带有-target-release命令行选项的JavacFileManager API 和ZipFileSystem API 来完成的。

  • 以下工具已被修改以利用对JavacFileManager API 和ZipFileSystem API 的更改:

    • javah:这会生成 C 头文件和源文件

    • schemagen:这是 Java 类中命名空间的模式生成器

    • wsgen:这是用于部署 Web 服务的解析器

  • javap 工具已更新以支持新的版本控制方案。

  • jdeps 工具已被修改以支持新的版本控制方案。

  • JAR 打包工具集已相应更新。此工具集包括pack200unpack200

  • 当然,JAR 工具也得到了增强,可以创建多版本 JAR 文件。

所有相关文档都已更新,以支持建立和支持新的多版本 JAR 文件格式所涉及的所有更改。

Java 级别的 JVM 编译器接口 [JEP-243]

JEP-243 旨在创建基于 Java 的JVM 编译器接口JVMCI)。JVMCI 允许 Java 编译器(必须是用 Java 编写的)作为 JVM 的动态编译器使用。

对 JVMCI 的渴望背后的原因是它将是一个高度优化的编译器,不需要低级语言功能。一些 JVM 子系统需要低级功能,例如垃圾收集和 bytemode 解释。因此,JVMCI 是用 Java 而不是 C 或 C++编写的。这带来了 Java 的一些最伟大特性的附带好处,如下所示:

  • 异常处理

  • 既是免费又强大的 IDE

  • 内存管理

  • 运行时可扩展性

  • 同步

  • 单元测试支持

由于 JVMCI 是用 Java 编写的,因此维护起来可能更容易。

JVMCI API 有三个主要组件:

  • 虚拟机数据结构访问

  • 与其元数据一起安装编译代码

  • 使用 JVM 的编译系统

JVMCI 实际上在 Java 8 中就已经存在,某种程度上。JVMCI API 只能通过一个适用于启动类路径上代码的类加载器来访问。在 Java 9 中,这种情况发生了变化。在 Java 9 中,它仍然将是实验性的,但更容易访问。为了启用 JVMCI,必须使用以下一系列命令行选项:

-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler - Djvmci.Compiler=<name of compiler>

Oracle 将 JVMCI 在 Java 9 中保持为实验性,以允许进一步的测试,并为开发者提供最大的保护。

BeanInfo 注解 [JEP-256]

JEP-256 专注于用更合适的注解替换@beanifo javadoc 标签。此外,这些新注解现在在运行时进行处理,以便可以动态生成BeanInfo类。Java 9 的模块化导致了这一变化。自定义BeanInfo类的创建已经简化,客户端库也已经模块化。

为了完全理解这个变化,在进一步探讨这个 JEP 之前,我们将回顾 JavaBeanBeanPropertySwingContainer

JavaBean

JavaBean 是一个 Java 类。与其他 Java 类一样,JavaBeans 是可重用的代码。它们在设计上独特,因为它们将多个对象封装到一个类中。一个 JavaBean 类必须遵循以下三个约定:

  • 构造函数不应该接受任何参数

  • 它必须是可序列化的

  • 它必须包含其属性的修改器和访问器方法

这里是一个 JavaBean 类的示例:

    public class MyBean implements java.io.Serializable 
    {
      // instance variables  
      private int studentId;
      private String studentName;

      // no-argument constructor
      public MyBean() 
      {

      }

      // mutator/setter 
      public void setStudentId(int theID)
      {
        this.studentId = theID;
      }

      // accessor/getter
      public int getStudentId()
      {
        return studentId;
      }

      // mutator/setter 
      public void setStudentName(String theName)
      {
        this.studentName = theName;
      }

      // accessor/getter
      public String getStudentName()
      {
        return studentName;
      }

    }

访问 JavaBean 类就像使用修改器和访问器方法一样简单。这可能对你来说并不陌生,但你可能不知道你精心编写的那些类被称为 JavaBean 类。

BeanProperty

BeanProperty 是一个注解类型。我们使用这个注解来指定一个属性,以便我们可以自动生成 BeanInfo 类。这是 Java 9 中的一个新注解。

BeanProperty 注解有以下可选元素:

  • boolean bound

  • String description

  • String[] enumerationValues

  • boolean expert

  • boolean hidden

  • boolean preferred

  • boolean required

  • boolean visualUpdate

SwingContainer

SwingContainer 是一个注解类型。我们使用这个注解来指定与 Swing 相关的属性,以便我们可以自动生成 BeanInfo 类。这是 Java 9 中的一个新注解。

SwingContainer 注解有以下可选元素:

  • String delegate

  • boolean value

现在我们已经回顾了 JavaBeanBeanPropertySwingContainer,让我们来看看 BeanInfo 类。

BeanInfo 类

大部分情况下,BeanInfo 类在运行时自动生成。例外的是 Swing 类。这些类基于 @beaninfo javadoc 标签生成 BeanInfo 类。这是在编译时完成的,而不是在运行时。在 Java 9 中,@beaninfo 标签已被 @interface JavaBean@interface BeanProperty@interface SwingContainer 注解所取代。

这些新注解用于根据前几节中提到的可选元素设置相应的属性。例如,以下代码片段设置了 SwingContainer 的属性:

    package javax.swing;

    public @interface SwingContainer
    {
      boolean value() default false;
      String delegate() default "";
    }

这为我们提供了三个好处:

  • 在 Bean 类中指定属性将比创建单个 BeanInfo 类要容易得多

  • 我们将能够移除自动生成的类

  • 使用这种方法,客户端库的模块化将变得更加容易

TIFF 图像输入/输出 [JEP-262]

JEP-262 非常直接。对于 Java 9,图像输入/输出插件已扩展以包括对 TIFF 图像格式的支持。ImageIO 类扩展了 Object 类,并是 Java SE 的一部分。该类包含用于编码和解码图像的几个方法。以下是静态方法列表:

方法 返回值
createImageInputStream(Object input) ImageInputStream
createImageOutputStream(Object output) ImageOutputStream
getCacheDirectory() 当前 CacheDirectory 的值
getImageReader(ImageWriter writer) ImageReader
getImageReaders(Object input) 当前 ImageReaders 迭代器
getImageReadersByFormatName(String formatName) 带有指定格式名称的当前 ImageReaders 迭代器
getImageReadersByMIMEType(String MIMEType) 当前指定 MIME 类型的 ImageReaders 迭代器
getImageReadersBySuffix(String fileSuffix) 带有指定后缀的当前 ImageReaders 迭代器
getImageTranscoders(ImageReader reader) 当前 ImageTranscoders 迭代器
getImageWriter(ImageReader reader) ImageWriter
getImageWriters(ImageTypeSpecifier type, String formatName) 可以编码到指定类型的当前 ImageWriters 迭代器
getImageWritersByFormatName(String formatName) 带有指定格式名称的当前 ImageWriters 迭代器
getImageWritersByMIMEType(String MIMEType) 当前指定 MIME 类型的 ImageWriters 迭代器
getImageWritersBySuffix(String fileSuffix) 带有指定后缀的当前 ImageWriters 迭代器
getReaderFileSuffixes() 当前读取器理解的文件后缀名字符串数组
getReaderFormatNames() 当前读取器理解的格式名称字符串数组
getReaderMIMETypes() 当前读取器理解的 MIME 类型字符串数组
getUseCache() UseCache
getWriterFileSuffixes() 当前编写器理解的文件后缀名字符串数组
getWriterFormatNames() 当前编写器理解的格式名称字符串数组
getWriterMIMETypes() 当前编写器理解的 MIME 类型字符串数组
read(File input) 带有 ImageReaderBufferedImage
read(ImageInputStream stream) 带有 ImageInputStreamImageReaderBufferedImage
read(InputStream input) 带有 InputStreamImageReaderBufferedImage
read(URL input) 带有 ImageReaderBufferedImage

存在几个不返回值或返回布尔值的静态方法:

方法 描述

| scanForPlugins() | 执行以下操作:

  • 在应用程序类路径中扫描插件

  • 加载插件服务提供程序类

  • 在 IIORegistry 中注册服务提供程序实例

|

setCacheDirectory(File cacheDirectory) 缓存文件将存储在这里。
setUseCache(boolean useCache) 此方法切换缓存是否基于磁盘。这适用于 ImageInputStreamImageOutputStream 实例。
write(RenderedImage im, String formatName, File output) 将图像写入指定的文件。
write(RenderedImage im, String formatName, ImageOutputStream output) 将图像写入 ImageOutputStream
write(RenderedImage im, String formatName, OutputStream output) 将图像写入 OutputStream

从提供的方法中可以看出,图像输入/输出框架为我们提供了一个方便的方式来使用图像编解码器。截至 Java 7,以下图像格式插件由 javax.imageio 实现:

  • BMP

  • GIF

  • JPEG

  • PNG

  • WBMP

如您所见,TIFF 并未出现在图像文件格式的列表中。TIFFs 是一种常见的文件格式,在 2001 年,随着 MacOS X 的发布,macOS 广泛使用了该格式。

Java 9 平台包括 TIFFs 的ImageReaderImageWriter插件。这些插件是用 Java 编写的,并打包在新的javax.imageio.plugins.tiff包中。

平台日志 API 和服务 [JEP-264]

Java 9 平台包括一个新的日志 API,使平台类能够记录消息。它有一个相应的服务来操作日志。在我们深入探讨关于日志 API 和服务的更新之前,让我们回顾一下在 Java 7 中引入的 java.util.logging.api

java.util.logging

java.util.logging 包包含类和接口,共同构成了 Java 的核心日志功能。此功能创建的目标如下:

  • 通过最终用户和系统管理员进行问题诊断

  • 通过现场服务工程师进行问题诊断

  • 通过开发组织进行问题诊断

如您所见,主要目的是为了使远程软件的维护成为可能。

java.util.logging 包包含两个接口:

  • public interface Filter

    • 目的:这提供了对记录数据的细粒度控制

    • 方法:

      • isLoggable(LogRecord record)
  • public interface LoggingMXBean

    • 目的:这是日志设施的管理接口

    • 方法:

      • getLoggerLevel(String loggerName)

      • getLoggerNames()

      • getparentLoggerName(String loggerName)

      • setLoggerLevel(String loggerName, String levelName)

下表提供了 java.util.logging 包的类,以及每个类在日志功能和管理方面的简要描述:

定义 描述
ConsoleHandler public class ConsoleHandler extends StreamHandler 将日志记录发布到 System.err
ErrorManager public class ErrorManager extends Object 用于在记录过程中处理错误
FileHandler public class FileHandler extends StreamHandler 文件记录
Formatter public abstract class Formatter extends Object 格式化 LogRecords
Handler public abstract class Handler extends Object 导出 Logger 消息
Level public class Level extends Object implements Serializable 控制日志级别。级别按降序排列为--严重、警告、信息、配置、精细、更精细和最精细
Logger public class Logger extends Object 记录消息
LoggingPermission public final class LoggingPermission extends BasicPermission SecurityManager会检查此权限
LogManager public class LogManager 用于在记录器和日志服务之间维护共享状态
LogRecord public class LogRecord extends Object implements Serializable 在处理器之间传递
MemoryHandler public class MemoryHandler extends Handler 在内存中缓冲请求
SimpleFormatter public class SimpleFormatter extends Formatter 提供可读的LogRecord元数据
SocketHandler public class SocketHandler extends StreamHandler 网络日志处理器
StreamHandler public class StreamHandler extends Handler 基于流的日志处理器
XMLFormatter public class XMLFormatter extends Formatter 将日志格式化为 XML

接下来,让我们回顾 Java 9 中做了哪些更改。

Java 9 中的日志记录

在 Java 9 之前,有多个日志架构可供选择,包括java.util.loggingSLF4JLog4J。后两者是具有独立门面和实现组件的第三方框架。这种模式在新 Java 9 平台中得到了复制。

Java 9 对java.base模块进行了更改,以便它将处理日志功能,而不是依赖于java.util.logging API。它有独立的门面和实现组件。这意味着当使用第三方框架时,JDK 只需要提供实现组件,并返回与请求的日志框架协同工作的平台日志记录器。

如以下插图所示,我们使用java.util.ServiceLoader API 来加载我们的LoggerFinder实现。如果使用系统类加载器找不到具体实现,JDK 将使用默认实现:

XML 目录 [JEP-268]

JEP 268,标题为 XML 目录,专注于创建一个标准的 XML 目录 API 来支持 OASIS XML 目录标准 v1.1。新的 API 定义了目录和目录解析抽象,以便 JAXP 处理器可以使用它们。在本节中,我们将查看以下内容:

  • OASIS XML 目录标准

  • JAXP 处理器

  • Java 9 之前的 XML 目录

  • Java 9 平台变更

OASIS XML 目录标准

XML (可扩展标记语言) 目录是由目录条目组成的 XML 文档。每个条目将一个标识符与另一个位置配对。OASIS 是一个非营利性联盟,其使命是推进开放标准。他们在 2005 年发布了 XML 目录标准,版本 1.1。此标准有两个基本用例:

  • 将外部标识符映射到 URI 引用

  • 将 URI 引用映射到另一个 URI 引用

这里是一个 XML 目录条目的示例:

    <public publicId="-//Packt Publishing Limited//Mastering Java 9//EN"
     uri="https://www.packtpub.com/application-development/mastering-java-9"/>

完整的 OASIS XML 目录标准可以在官方网站找到:www.oasis-open.org/committees/download.php/14809/xml-catalogs.html

JAXP 处理器

Java XML 处理 API 被称为 JAXP。正如其名称所暗示的,此 API 用于解析 XML 文档。有四个相关接口:

  • DOM:文档对象模型解析

  • SAX:XML 解析的简单 API

  • StAX:XML 解析的流式 API

  • XSLT:转换 XML 文档的接口

Java 9 之前的 XML 目录

Java 平台自 JDK 6 以来就有内部目录解析器。没有公开的 API,因此使用外部工具和库来访问功能。进入 Java 9,目标是使内部目录解析器成为通用 API 的标准,以简化支持。

Java 9 平台变更

新的 XML 目录 API,随 Java 9 发布,遵循 OASIS XML 目录标准,v1.1。以下是功能和能力亮点:

  • 实现了EntityResolver

  • 实现URIResolver

  • 通过CatalogManager创建 XML 目录

  • CatalogManager将用于创建CatalogResolvers

  • 遵循 OASIS 开放目录文件语义

    • 将外部标识符映射到 URI 引用

    • 将 URI 引用映射到另一个 URI 引用

  • CatalogResolvers将实现 JAXP EntityResolver接口

  • CatalogResolvers将实现 JAXP URIResolver接口

  • SAX XMLFilter将由解析器支持。

由于新的 XML 目录 API 将是公开的,Java 9 之前的内部目录解析器将被移除,因为它将不再必要。

集合的便利工厂方法 [JEP-269]

Java 编程语言不支持集合字面量。在 2013 年提出将该功能添加到 Java 平台,并在 2016 年重新审视,但它仅作为研究提案曝光,而不是未来的实现。

Oracle 对集合字面量的定义是“一种评估为聚合类型的语法表达式形式,例如数组、列表或映射”(openjdk.java.net/jeps/186)。

当然,直到 Java 9 发布。在 Java 编程语言中实现集合字面量据报道有以下好处:

  • 性能提升

  • 提高安全性

  • 减少样板代码

即使不是研究小组的一部分,我们对 Java 编程语言的知识也让我们了解到额外的优势:

  • 编写更短代码的能力

  • 编写空间高效代码的能力

  • 使集合字面量不可变的能力

让我们看看两个案例——在 Java 9 之前使用集合,然后在新 Java 平台中对集合字面量的新支持。

在 Java 9 之前使用集合

这里是一个示例,说明我们如何在 Java 9 之前创建自己的集合。这个第一类定义了PlanetCollection的结构。它有以下组件:

  • 单个实例变量

  • 一个参数的构造函数

  • 修改器/设置方法

  • 访问器/获取方法

  • 打印对象的函数

这里是实现前面列出的构造函数和方法的代码:

    public class PlanetCollection 
    {
      // Instance Variable
      private String planetName;

      // constructor
      public PlanetCollection(String name)
      {
        setPlanetName(name);
      }

      // mutator
      public void setPlanetName(String name)
      {
        this.planetName = name;
      }

      // accessor
      public String getPlanetName()
      {
        return this.planetName;
      }

      public void print()
      {
        System.out.println(getPlanetName());
      }
    }

现在,让我们看看填充集合的驱动类:

    import java.util.ArrayList;

    public class OldSchool 
    {
      private static ArrayList<PlanetCollection> myPlanets =
        new ArrayList<>();

      public static void main(String[] args) 
      {
        add("Earth");
        add("Jupiter");
        add("Mars");
        add("Venus");
        add("Saturn");
        add("Mercury");
        add("Neptune");
        add("Uranus");
        add("Dagobah");
        add("Kobol");

        for (PlanetCollection orb : myPlanets)
        {
          orb.print();
        }

      }

      public static void add(String name)
      {
        PlanetCollection newPlanet = new PlanetCollection(name);
        myPlanets.add(newPlanet);
      }
    }

下面是此应用程序的输出:

图片

很遗憾,这段代码非常冗长。我们使用静态初始化块而不是字段初始化来填充我们的集合。还有其他填充列表的方法,但它们都比应有的冗长。这些其他方法还有其他问题,例如需要创建额外的类、使用晦涩的代码和隐藏的引用。

让我们现在看看由新的 Java 9 平台提供的此问题的解决方案。我们将在下一节中查看新内容。

使用新的集合字面量

为了纠正目前创建集合所需的代码冗长性,我们需要用于创建集合实例的库 API。查看上一节中的我们的 Java 9 之前的代码片段,然后考虑以下可能的重构:

    PlanetCollection<String> myPlanets = Set.of(
      "Earth",
      "Jupiter",
      "Mars",
      "Venus",
      "Saturn",
      "Mercury",
      "Neptune",
      "Uranus",
      "Dagobah",
      "Kobol");

这段代码高度可读,且不冗长。

新的实现将包括以下接口上的静态工厂方法:

  • List

  • Map

  • Set

因此,我们现在能够创建不可修改的 List 集合、Map 集合和 Set 集合的实例。它们可以使用以下语法进行实例化:

  • List.of(a, b, c, d, e);

  • Set.of(a, b, c, d, e);

  • Map.of();

Map 集合将有一组固定参数。

平台特定桌面功能 [JEP-272]

令人兴奋的 JEP-272 是创建一个新的公共 API,这样我们就可以编写具有访问平台特定桌面功能的应用程序。这些功能包括与任务栏/托盘交互以及监听应用程序和系统事件。

macOS X 的 com.apple.eawt 包是一个内部 API,从 Java 9 开始不再可访问。为了支持 Java 9 的新嵌入式平台特定桌面功能,apple.applescript 类将从 Java 平台中移除,而不提供替代方案。

此项工作有几个目标:

  • 创建一个公共 API 来替换 com.apple.{east,eio} 中的功能

  • 确保 OS X 开发者不会失去功能。为此,Java 9 平台为以下包提供了替代方案:

    • com.apple.eawt

    • com.apple.eio

  • 为平台(即 Windows 和 Linux)以及 OS X 提供一个接近通用的功能集。这些通用功能包括:

    • 具有事件监听器的登录/注销处理器

    • 具有事件监听器的屏幕锁定处理器

    • 包括以下任务栏/托盘操作:

      • 请求用户注意

      • 指示任务进度

      • 操作快捷键

新的 API 将被添加到 java.awt.Desktop 类中。

增强方法句柄 [JEP-274]

增强方法句柄 JEP-274 的目的是改进以下列出的类,以便通过改进的优化使常见用法更容易:

  • MethodHandle

  • MethodHandles

  • MethodHandles.Lookup

列出的类都是 java.lang.invoke 包的一部分,该包作为 Java 9 平台的一部分进行了更新。这些改进是通过使用查找细化以及 MethodHandle 组合 for 循环和 try...finally 块实现的。

在本节中,我们将探讨有关 JEP-274 的以下内容:

  • 增强的原因

  • 查找函数

  • 参数处理

  • 额外的组合

增强的原因

此增强源于开发者的反馈和使 MethodHandleMethodHandlesMethodHandles.Lookup 类更容易使用的愿望。还有增加额外用例的呼吁。

这些更改带来了以下好处:

  • MethodHandle API 的使用中启用精度

  • 实例化减少

  • 增加了 JVM 编译器优化

查找函数

关于查找函数的更改,包括以下内容:

  • MethodHandles 现在可以绑定到接口中的非抽象方法

  • 查找 API 允许从不同的上下文进行类查找

MethodHandles.Lookup.findSpecial(Class<?> refs, String name, MethodType type, Class<?> specialCaller) 类已被修改,允许在接口上定位可调用的超方法。

此外,以下方法已添加到 MethodHandles.Lookup 类中:

  • Class<?> findClass(String targetName)

  • Class<?> accessClass(Class<?> targetClass)

参数处理

MethodHandle 参数处理进行了三项更新,以改进 Java 9 平台。以下是对这些更改的概述:

  • 使用 foldArguments(MethodHandle target, MethodHandle combinator) 进行参数折叠之前没有位置参数。

    • 使用 MethodHandle.asCollector(Class<?> arrayType, int arrayLength) 方法进行参数收集之前不支持将参数收集到数组中,除了尾随元素。现在已更改,并在 Java 9 中添加了一个额外的 asCollector 方法来支持该功能。
  • 使用 MethodHandle.asSpreader(Class<?> arrayType, int arrayLength) 方法通过反向收集参数的方式将尾随数组的元素扩展到多个参数中。参数扩展已被修改以支持在方法签名中的任何位置扩展数组。

下节将提供更新后的 asCollectorasSpreader 方法的新的方法定义。

额外的组合

为了支持 MethodHandleMethodHandlesMethodHandles.Lookup 类在 Java 9 平台上的易用性和优化,已添加以下附加组合:

  • 泛型循环抽象:

    • MethodHandle loop(MethodHandle[] . . . clauses)
  • While 循环:

    • MethodHandle whileLoop(MethodHandle init, MethodHandle pred, MethodHandle body)
  • Do...while 循环:

    • `MethodHandle doWhileLoop(MethodHandle init, MethodHandle body, MethodHandle pred)`
  • 计数循环:

    • MethodHandle countedLoop(MethodHandle iterations, MethodHandle init, MethodHandle body)
  • 数据结构迭代:

    • MethodHandle iteratedLoop(MethodHandle iterator, MethodHandle init, MethodHandle body)
  • Try...finally块:

    • MethodHandle tryFinally(MethodHandle target, MethodHandle cleanup)
  • 参数处理:

    • 参数展开:

      • MethodHandle asSpreader(int pos, Class<?> arrayType, int arrayLength)
    • 参数收集:

      • MethodHandle asCollector(int pos, Class<?> arrayType, int arrayLength)
    • 参数折叠:

      • MethodHandle foldArguments(MethodHandle target, int pos, MethodHandle combiner)

增强弃用 [JEP-277]

表达弃用有两种设施:

  • @Deprecated注解

  • @deprecated javadoc 标签

这些设施分别在 Java SE 5 和 JDK 1.1 中引入。@Deprecated注解的目的是注释那些被认为危险且/或存在更好选择的程序组件。这是预期用途。实际使用情况各异,包括因为警告仅在编译时提供;几乎没有理由忽略被注释的代码。

增强弃用 JEP-277 被采纳,以向开发者提供有关规范文档中 API 预期处置的更清晰信息。这项 JEP 的工作还产生了一个分析程序对弃用 API 使用的工具。

为了支持这种信息保真度,以下组件已被添加到java.lang.Deprecated注解类型中:

  • forRemoval():

    • 如果 API 元素已计划在未来删除,则返回布尔值true

    • 如果 API 元素尚未计划在未来删除但已弃用,则返回布尔值false

    • 默认值为false

  • since():

    • 返回一个包含发布或版本号的字符串,此时指定的 API 被标记为弃用

@Deprecated 注解真正意味着什么

当 API 或 API 内的方法被标记为@Deprecated注解时,通常存在以下一个或多个条件:

  • API 中存在错误,目前没有计划修复它们

  • 使用 API 可能会产生错误

  • API 已被另一个 API 取代

  • API 是实验性的

摘要

在本章中,我们介绍了 16 个被纳入 Java 9 平台的 JEP。这些 JEP 涵盖了广泛的工具和 API 更新,使使用 Java 开发更容易,并为我们的程序提供了更大的优化可能性。我们的审查包括对新 HTTP 客户端、Javadoc 和 Doclet API 的更改、新的 JavaScript 解析器、JAR 和 JRE 更改、新的 Java 级别 JVM 编译器接口、对 TIFF 图像的新支持、平台日志记录、XML 目录支持、集合以及新的平台特定桌面功能。我们还研究了方法处理和弃用注解的增强。

在下一章中,我们将介绍 Java 9 平台引入的并发增强功能。我们的主要关注点将是流类 API 提供的响应式编程支持。我们还将探讨 Java 9 中引入的其他并发增强功能。

第十二章:并发与响应式编程

在上一章中,我们介绍了几个被纳入 Java 9 平台的Java 增强提案JEPs)。这些 JEPs 代表了一系列工具和 API 的更新,使得使用 Java 进行开发更加容易,并为我们的 Java 应用程序提供了更大的优化可能性。我们探讨了新的 HTTP 客户端、Javadoc 和 Doclet API 的变更、新的 JavaScript 解析器、JAR 和 JRE 的变更、新的 Java 级别 JVM 编译器接口、对 TIFF 图像的新支持、平台日志记录、XML 目录支持、集合以及新的平台特定桌面功能。我们还探讨了方法处理增强和弃用注解。

在本章中,我们将介绍 Java 9 平台引入的并发增强。我们的主要关注点将是响应式编程的支持,这是由Flow类 API 提供的并发增强。响应式编程是 Java 9 的一个新概念,因此我们将对该主题采取探索性方法。我们还将探讨 Java 9 中引入的其他并发增强。

具体来说,我们将涵盖以下主题:

  • 响应式编程

  • 新的Flow API

  • 额外的并发更新

  • 自旋等待提示

响应式编程

响应式编程是指应用程序对异步数据流进行响应,当它发生时。以下图像展示了流程:

图片

响应式编程不是一个只有学者使用的花哨的软件工程术语。实际上,它是一种编程模型,与更常见的应用程序遍历内存中数据的方法相比,它可以带来更高的效率。

响应式编程还有更多内容。首先,让我们考虑数据流是以异步方式由发布者提供给订阅者的。

数据流是字符串和原始数据类型的二进制输入/输出。DataInput接口用于输入流,而DataOutput接口用于输出流。

处理器,或处理器链,可以用来转换数据流,而不会影响发布者或订阅者。在以下示例中,处理器在数据流上工作,而不涉及发布者订阅者,甚至不需要它们知道:

图片

除了更高的效率外,响应式编程还代表了几项额外的优势,以下将重点介绍:

  • 代码库可以更简洁,这使得它:

    • 更容易编码

    • 更容易维护

    • 更容易阅读

  • 流处理导致内存效率提升

  • 这是一个适用于各种编程应用的解决方案

  • 需要编写的样板代码更少,因此可以将开发时间集中在编程核心功能上

  • 以下类型的编程需要更少的时间和代码:

    • 并发

    • 低级线程

    • 同步

响应式编程标准化

软件开发的许多方面都有标准,响应式编程也不例外。有一个Reactive Streams倡议用于标准化异步流处理。在 Java 的上下文中,具体关注的是 JVM 和 JavaScript。

Reactive Streams 倡议旨在解决如何管理线程之间数据流交换的问题。如您从上一节所回忆的那样,处理器概念基于对发布者或接收者没有影响。这个无影响的要求规定以下内容是不必要的:

  • 数据缓冲

  • 数据转换

  • 转换

标准的基本语义定义了数据流元素传输的规范。这个标准是专门为与 Java 9 平台一起交付而设立的。Reactive Streams 包括一个库,可以帮助开发者从org.reactivestreamsjava.util.concurrent.Flow命名空间进行转换。

在响应式编程和 Reactive Streams 标准化中取得成功的关键是理解相关的术语:

术语 描述
需求 需求指的是订阅者请求更多元素,同时也指尚未由发布者满足的请求元素的总数。
需求 需求也指尚未由发布者满足的请求元素的总数。
外部同步 线程安全的外部访问协调。
非阻塞 如果方法快速执行而不需要大量计算,则称这些方法为非阻塞方法。非阻塞方法不会延迟订阅者的线程执行。
无操作 无操作执行是可以反复调用而不影响调用线程的执行。
响应性 这个术语指的是组件的响应能力。
正常返回 正常返回指的是没有错误发生的情况,即正常状态。onError方法是标准允许的唯一通知订阅者失败的方式。

| 信号 | 以下方法之一:

  • cancel

  • onComplete

  • onError

  • onNext

  • onSubscribe

  • request

|

您可以从 Maven Central(search.maven.org)获取标准。以下是截至本书出版日期的 Maven Central 上的标准:

    <dependency>
      <groupId>org.reactivestreams</groupId>
      <artifactId>reative-streams</artifactId>
      <version>1.0.1</version>
    </dependency>

    <dependency>
      <groupId>org.reactivestreams</groupId>
      <artifact>reactive-streams-tck</artifactId>
      <version>1.0.0</version>
      <scope>test</scope>
    </dependency>

在下一节中,我们将探讨 Java 9 平台中的 Flow API,因为它们对应于 Reactive Streams 规范。

新的 Flow API

Flow类是java.util.concurrent包的一部分。它帮助开发者将响应式编程融入他们的应用程序。该类有一个方法defaultBufferSize()和四个接口。

defaultBufferSize()是一个静态方法,它返回发布和订阅缓冲的默认缓冲区大小。这个默认值是256,并以int的形式返回。让我们看看这四个接口。

Flow.Publisher接口

Flow.Publisher接口是一个函数式接口。一个Publisher是向订阅者发送数据的生产者:

    @FunctionalInterface
    public static interface Flow.Publisher<T>

这个函数式接口可以作为 lambda 表达式赋值的目标。它只接受一个参数--订阅项的类型<T>。它有一个方法:

  • void onSubscribe(Flow.Subscription subscription)

Flow.Subscriber接口

Flow.Subscriber接口用于接收消息,其实现如下:

    public static interface Flow.Subscriber<T>

这个接口被设置为接收消息。它只接受一个参数--订阅项的类型<T>。它有以下方法:

  • void onComplete()

  • void onError(Throwable throwable)

  • void onNext(T item)

  • void onSubscribe(Flow.Subscription subscription)

Flow.Subscription接口

Flow.Subscription接口确保只有订阅者会接收到请求的内容。同时,您将在此处看到,订阅可以在任何时候取消:

    public static interface Flow.Subscription

这个接口不接受任何参数,是控制Flow.PublisherFlow.Subscriber实例之间消息的链接。它有以下方法:

  • void cancel()

  • void request(long n)

Flow.Processor接口

Flow.Processor接口可以作为SubscriberPublisher使用。实现如下:

    static interface Flow.Processor<T,R> extends Flow.Subscriber<T>,
     Flow.Publisher<R>

这个接口接受两个参数--订阅项的类型<T>和发布项的类型<R>。它没有自己的方法,但继承自java.util.concurrent.Flow.Publisher的以下方法:

  • void subscribe(Flow.Subscriber<? super T> subscriber)

Flow.Processor还从java.util.concurrent.Flow.Subscriber接口继承了以下方法:

  • void onComplete()

  • void onError(Throwable throwable)

  • void onNext(T item)

  • void onSubscribe(Flow.Subscription subscription)

示例实现

在任何响应式编程的实现中,我们都会有一个请求数据的Subscriber和一个提供数据的Publisher。让我们首先看看一个示例Subscriber实现:

    import java.util.concurrent.Flow.*;

    public class packtSubscriber<T> implements Subscriber<T>
    {
      private Subscription theSubscription;

      // We will override the four Subscriber interface methods

      @Override
      public void onComplete()
      {
        System.out.println("Data stream ended");
      }

      @Override
      public void onError(Throwable theError)
      {
        theError.printStackTrace();
      }

      @Override
      public void onNext(T theItem)
      {
        System.out.println("Next item received: " + theItem);
        theSubscription.request(19);  // arbitrary number for
         example purposes
      }

      @Override
      public void onSubscribe(Subscription theSubscription)
      {
        this.theSubscription = theSubscription;
        theSubscription.request(19);
      }

    } 

如您所见,实现Subscriber并不困难。重头戏是在SubscriberPublisher之间的处理器中完成的。让我们看看一个示例实现,其中Publisher向订阅者发布数据流:

    import java.util.concurrent.SubsmissionPublisher;

    . . . 

    // First, let's create a Publisher instance
    SubmissionPublisher<String> packtPublisher = new 
     SubmissionPublisher<>();

    // Next, we will register a Subscriber
    PacktSubscriber<String> currentSubscriber = new 
     PacktSubscriber<>();
    packtPublisher.subscribe(currentSubscriber);

    // Finally, we will publish data to the Subscriber and 
       close the publishing effort
    System.out.println("||---- Publishing Data Stream ----||");
    . . . 
    packtPublisher.close();
    System.out.println("||---- End of Data Stream Reached ----||");

其他并发更新

更多并发更新 Java 增强提案,JEP 266,旨在改善 Java 中的并发使用。在本节中,我们将简要探讨 Java 并发的概念,并查看对 Java 9 平台的相关增强:

  • Java 并发

  • 支持响应式流

  • CompletableFuture API 增强

Java 并发

在本节中,我们将从并发的一个简要解释开始,然后查看系统配置,涵盖 Java 线程,然后查看并发改进。

并发解释

并发处理自 20 世纪 60 年代以来就已经存在。在那些形成年份,我们已经有允许多个进程共享单个处理器的系统。这些系统更明确地定义为伪并行系统,因为它们看起来像多个进程正在同时执行。我们今天的计算机仍然以这种方式运行。20 世纪 60 年代和今天之间的区别在于,我们的计算机可以有多个 CPU,每个 CPU 有多个核心,这更好地支持了并发。

并发和并行性经常被互换使用。并发是指多个进程重叠,尽管开始和结束时间可能不同。并行性发生在任务同时开始、运行和停止时。

系统配置

需要考虑几种不同的处理器配置。本节介绍了两种常见的配置。第一种配置是共享内存,如下所示:

图片

如您所见,共享内存系统配置具有多个处理器,它们都共享一个公共的系统内存。第二个特色系统配置是分布式内存系统:

图片

在分布式内存系统中,每个处理器都有自己的内存,每个单独的处理器与其他处理器完全连接,从而形成一个完全连接的分布式系统。

Java 线程

Java 中的线程是一个程序执行,它是内置在 JVM 中的。Thread类是java.lang包的一部分(java.lang.Thread)。线程有优先级,它控制 JVM 执行它们的顺序。虽然这个概念很简单,但实现并不简单。让我们先仔细看看Thread类。

线程类有两个嵌套类:

  • public static enum Thread.State

  • public static interface Thread.UncaughtExceptionHandler

有三个实例变量用于管理线程优先级:

  • public static final int MAX_PRIORITY

  • public static final int MIN_PRIORITY

  • public static final int NORM_PRIORITY

线程类有八个构造函数,它们都会分配一个新的Thread对象。以下是构造函数的签名:

  • public Thread()

  • public Thread(Runnable target)

  • public Thread(Runnable target, String name)

  • public Thread(String name)

  • public Thread(ThreadGroup group, Runnable target)

  • public Thread(ThreadGroup group, Runnable target, String name)

  • public Thread(ThreadGroup group, Runnable target, String name, long stackSize)

  • public Thread(ThreadGroup group, String name)

线程类还有 43 个方法,其中六个已被弃用。其余的方法在此列出,除了访问器和修改器,它们将单独列出。您可以查阅文档以了解每个方法的详细信息:

  • public static int activeCount()

  • public final void checkAccess()

  • protected Object clone() throws CloneNotSupportedException

  • public static Thread currentThread()

  • public static void dumpStack()

  • public static int enumerate(Thread[] array)

  • public static boolean holdsLock(Object obj)

  • public void interrupt()

  • public static boolean interrupted()

  • public final boolean isAlive()

  • public final boolean isDaemon()

  • public boolean isInterrupted()

  • 等待方法:

    • public final void join() throws InterruptedException

    • public final void join(long millis) throws InterruptedException

    • public final void join(long millis, int nano) throws InterruptedException

  • public void run()

  • 睡眠方法:

    • public static void sleep(long mills) throws InterruptedException

    • public static void sleep(long mills, int nano) throws InterruptedException

  • public void start()

  • public String toString()

  • public static void yield()

下面是Thread类的访问器/获取器和修改器/设置器的列表:

  • 访问器/获取器:

    • public static Map<Thread, StackTraceElement[]> getAllStacktraces()

    • public ClassLoader getContextClassLoader()

    • public static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()

    • public long getId()

    • public final String getName()

    • public final int getPriority()

    • public StackTraceElement[] getStackTrace()

    • public Thread.State getState()

    • public final ThreadGroup getThreadGroup()

    • public Thread.UncaughtExceptionHandler getUncaughtExceptionHandler()

  • 修改器/设置器:

    • public void setContextClassLoader(ClassLoader cl)

    • public final void setDaemon(boolean on)

    • public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

    • public final void setName(String name)

    • public final void setPriority(int newPriority)

    • public void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

在 Java 中,并发通常被称为多线程。如前所述,管理线程,尤其是多线程,需要极高的控制精度。Java 采用了一些技术,包括使用锁。代码段可以被锁定,以确保在任何给定时间只有一个线程可以执行该代码。我们可以使用synchronized关键字来锁定类和方法。以下是如何锁定整个方法的示例:

    public synchronized void protectedMethod()
    {
      . . . 
    }

下面的代码片段演示了如何使用synchronized关键字在方法内锁定代码块:

    . . . 
    public class unprotectedMethod()
    {
      . . . 
      public int doSomething(int tValue) 
      {
        synchronized (this)
        {
          if (tValue != 0)
          {
            // do something to change tValue
            return tValue;
          }
        }
      } 
    }

并发改进

在我们的 Java 应用程序中,使用多线程的能力可以大大提高效率并利用现代计算机日益增强的处理能力。Java 中使用线程为我们提供了在并发控制中的高度粒度。

线程是 Java 并发功能的核心。我们可以在 Java 中通过定义一个run方法并实例化一个Thread对象来创建一个线程。有两种方法来完成这一组任务。我们的第一个选项是扩展Thread类并重写Thread.run方法。以下是一个示例:

    . . .
    class PacktThread extends Thread
    {
      . . .
      public void run()
      {
        . . . 
      }
    }

    . . . 

    Thread varT = new PacktThread();

    . . .

    // This next line is start the Thread by executing
       the run() method.
    varT.start();

    . . . 

第二种方法是创建一个实现Runnable接口的类,并将该类的实例传递给Thread构造函数。以下是一个示例:

    . . . 
    class PacktRunner implements Runnable
    {
       . . .
      public void run()
      {
        . . .
      }
    }

    . . . 

    PacktRunner varR = new PacktRunner();
    Thread varT = new Thread(varR);

    . . .

    // This next line is start the Thread by executing the 
       run() method.
    varT.start();

    . . . 

这两种方法效果相同,您使用哪一种取决于开发者的选择。当然,如果您需要额外的灵活性,第二种方法可能是一个更好的选择。您可以尝试这两种方法来帮助您做出决定。

CompletableFuture API 增强

CompleteableFuture<T>类是java.util.concurrent包的一部分。该类扩展了Object类并实现了Future<T>CompletionStage<T>接口。此类用于注释可以完成的线程。我们可以使用CompletableFuture类来表示未来的结果。当使用complete方法时,该未来的结果可以被完成。

重要的是要意识到,如果有多个线程同时尝试完成(完成或取消),除了一个之外,其他都会失败。让我们看看这个类,然后再看看增强功能。

类详细信息

CompleteableFuture<T>类有一个内部类,用于标记异步任务:

    public static interface
     CompletableFuture.AsynchronousCompletionTask

CompleteableFuture<T>类的构造函数必须与提供的构造函数签名同步,并且不接受任何参数。该类有以下方法,按它们返回的内容组织。

返回一个CompletionStage

  • public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)

  • public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)

  • public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action, Executor executor)

  • public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn)

  • public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn)

  • public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn, Executor executor)

  • public static <U> CompletionStage<U> completedStage(U value)

  • public static <U> CompletionStage<U> failedStage(Throwable ex)

  • public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)

  • public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn)

  • public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor)

  • public CompletionStage<T> minimalCompletionStage()

  • `public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other, Runnable action)`

  • public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action)

  • public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other, Runnable action, Executor executor)

  • public CompletableFuture<Void> runAfterEither(CompletionStage<?> other, Runnable action)

  • public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action)

  • public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other, Runnable action, Executor executor)

  • public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)

  • public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)

  • public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor)

这些方法返回一个 CompletionStage:

  • public CompletableFuture<Void> thenAccept(Consumer<? super T> action)

  • public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)

  • public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)

  • public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action)

  • public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action)

  • public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action, Executor executor)

  • `public <U> CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn)`

  • public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn)

  • public <U> CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn, Executor executor)

  • public <U, V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn)

  • public <U, V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn)

  • public <U, V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn, Executor executor)

  • public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)

  • public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn)

  • public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor)

  • public CompletableFuture<Void> thenRun(Runnable action)

  • public CompletableFuture<Void>thenRunAsync(Runnable action)

  • public CompletableFuture<Void>thenRunAsync(Runnable action, Executor executor)

这些方法返回一个 CompleteableFuture:

  • public static CompletableFuture<Void> allOf(CompletableFuture<?>...cfs)

  • public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

  • public CompletableFuture<T> completeAsync(Supplier<? extends T> supplier, Executor executor)

  • public CompletableFuture<T> completeAsync(Supplier<? extends T> supplier)

  • `public static <U> CompletableFuture<U> completedFuture(U value)`

  • public CompletableFuture<T> completeOnTimeout(T value, long timeout, TimeUnit unit)

  • public CompletableFuture<T> copy()

  • public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

  • public static <U> CompletableFuture<U> failedFuture(Throwable ex)

  • public <U> CompletableFuture<U> newIncompeteFuture()

  • public CompletableFuture<T> orTimeout(long timeout, TimeUnit unit)

  • public static ComletableFuture<Void> runAsync(Runnable runnable)

  • public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)

  • public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)

  • public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

  • public CompletableFuture<T> toCompletableFuture()

这些方法返回一个Executor

  • public Executor defaultExecutor()

  • public static Executor delayedExecutor(long delay, Timeunit unit, Executor executor)

  • public static Executor delayedExecutor(long delay, Timeunit unit)

这些方法返回一个布尔值

  • public boolean cancel(boolean mayInterruptIfRunning)

  • public boolean complete(T value)

  • public boolean completeExceptionally(Throwable ex)

  • public boolean isCancelled()

  • public boolean isCompletedExceptionally()

  • public boolean isDone()

没有返回类型:

  • public void obtrudeException(Throwable ex)

  • public void obtrudeValue(T value)

其他方法:

  • public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException

  • public T get() throws InterruptedException, ExecutionException

  • public T getNow(T valueIfAbsent)

  • public int getNumberOfDependents()

  • public T join()

  • public String toString()

增强功能:

CompleteableFuture<T>类作为 Java 9 平台的一部分获得了以下增强:

  • 添加了基于时间增强:

    • 这使得基于超时的完成成为可能

    • 现在也支持延迟执行

  • 对子类的重要增强:

    • 扩展CompletableFuture更加容易

    • 子类支持替代默认执行器

特别是,以下方法是在 Java 9 中添加的:

  • newIncompleteFuture()

  • defaultExecutor()

  • copy()

  • minimalCompletionStage()

  • completeAsync()

  • orTimeout()

  • `completeOnTimeout()`

  • delayedExecutor()

  • completedStage()

  • failedFuture()

  • failedStage()

自旋等待提示:

在并发中,我们需要确保等待执行的线程实际上被执行。自旋等待的概念是一个不断检查真实条件的过程。Java 增强提案 285 的目的是创建一个 API,允许 Java 代码发出自旋循环正在执行的提示。

虽然这不是每个 Java 开发者都会使用的功能,但它对于底层编程可能很有用。提示系统仅发出提示——指示,并不执行其他操作。添加这些提示的理由包括以下假设:

  • 使用自旋提示时,自旋循环的动作时间可以得到改善

  • 使用自旋提示将减少线程间的延迟

  • CPU 功耗将会降低

  • 硬件线程将执行得更快

这个提示功能将被包含在一个新的onSpinWait()方法中,作为java.lang.Thread类的一部分。以下是一个实现onSpinWait()方法的示例:

    . . . 

    volatile boolean notInReceiptOfEventNotification; 

    . . . 

    while ( notInReceiptOfEventNotification );
    {
      java.lang.Thread.onSpinWait();
    }

    // Add functionality here to read and process the event

    . . . 

摘要

在本章中,我们介绍了 Java 9 平台引入的并发增强功能。我们深入探讨了并发,既作为 Java 的核心概念,也着眼于 Java 9 所提供的内容。我们还探讨了支持响应式编程的Flow类 API,这是 Java 9 中的新概念。此外,我们还探讨了 Java 9 中的并发增强和新引入的自旋等待提示。

在下一章中,我们将突出介绍 Java 9 引入的安全增强功能,以及实际示例。

第十三章:安全增强

在上一章中,我们介绍了 Java 9 平台引入的并发增强。我们深入探讨了并发,既作为 Java 的核心概念,也作为 Java 9 的系列增强。我们还探讨了支持响应式编程的 Flow 类 API,这是 Java 9 中的新概念。此外,我们还探讨了 Java 9 中引入的并发增强和新 Spin-Wait 指令。

在本章中,我们将查看对 JDK 进行的一些涉及安全性的小改动。这些改动的大小并不反映它们的显著性。Java 9 平台引入的安全增强为开发者提供了编写和维护比以前更安全的应用程序的能力。

具体来说,在本章中,我们将回顾以下内容领域:

  • 数据报传输层安全

  • 创建 PKCS12 密钥库

  • 提高安全应用程序性能

  • TLS 应用层协议协商扩展

  • 利用 CPU 指令进行 GHASH 和 RSA

  • TLS 的 OCSP 钩子

  • 基于 DRBG 的 SecureRandom 实现

数据报传输层安全

数据报传输层安全DTLS),是一种通信协议。该协议为基于数据报的应用提供一层安全。DTLS 允许安全通信,并基于 传输层安全TLS)协议。嵌入式安全有助于确保消息不被伪造、篡改或窃听。

让我们回顾一下相关术语:

  • 通信协议:一组规则,用于规范信息的传输。

  • 数据报:一个结构化传输单元。

  • 窃听:对传输中的数据包进行未检测到的监听。

  • 伪造:发送伪造发送者的数据包。

  • 网络数据包:用于传输的格式化数据单元。

  • 干扰:在发送者发送数据包后和预期接收者接收它们之前更改数据包。

  • TLS 协议:最常见的网络安全协议。例如,使用 IMPA 和 POP 进行电子邮件。

DTLS Java 增强提案 219 旨在为 DTLS 版本 1.0 和 1.2 创建 API。

在接下来的章节中,我们将查看 DTLS 的每个版本,1.0 和 1.2,然后回顾对 Java 9 平台的改变。

DTLS 协议版本 1.0

DTLS 协议版本 1.0 于 2006 年建立,为数据报协议提供通信安全。以下是基本特征:

  • 允许客户端/服务器应用程序通信,但不允许:

    • 窃听

    • 干扰

    • 消息伪造

  • 基于 TLS 协议

  • 提供安全保证

  • DLS 协议的数据报语义得到保留

以下图表说明了 传输层SSL/TLS 协议层和每层协议的整体架构中的位置:

图片

DTLS 协议版本 1.0 提供了详细的规范,主要覆盖领域如下:

  • 密码:

    • 反重放分组密码

    • 新的密码套件

    • 标准或空流密码

  • 拒绝服务对策

  • 握手:

    • 消息格式

    • 协议

    • 可靠性

  • 消息:

    • 分片和重组

    • 无损消息

    • 大小

    • 超时和重传

    • 数据包丢失

  • 路径最大传输单元PMTU)发现

  • 记录层

  • 记录有效载荷保护

  • 重新排序

  • 重放检测

  • 传输层映射

DTLS 协议版本 1.2

DTLS 协议版本 1.2 于 2012 年 1 月发布,由互联网工程任务组IETF)版权所有。本节分享了代码示例,说明了 1.2 版本中的变化。

以下代码说明了 TLS 1.2 握手消息头。此格式支持:

  • 消息分片

  • 消息丢失

  • 重新排序

    // Copyright (c) 2012 IETF Trust and the persons identified as
       authors of the code. All rights reserved.

    struct 
    {
      HandshakeType msg_type;
      uint24 length;
      uint16 message_seq;                           // New field
      uint24 fragment_offset;                       // New field
      uint24 fragment_length;                       // New field
      select (HandshakeType) 
      {
        case hello_request: HelloRequest;
        case client_hello:  ClientHello;
        case hello_verify_request: HelloVerifyRequest;  // New type
        case server_hello:  ServerHello;
        case certificate:Certificate;
        case server_key_exchange: ServerKeyExchange;
        case certificate_request: CertificateRequest;
        case server_hello_done:ServerHelloDone;
        case certificate_verify:  CertificateVerify;
        case client_key_exchange: ClientKeyExchange;
        case finished: Finished;
      } body;
    } Handshake;

本节中展示的代码来自 DTLS 协议文档,并按照 IETF 的《关于 IETF 文档的法律条款》在此重新发布。

记录层包含我们打算发送到记录中的信息。信息最初在DTLSPlaintext结构内部,然后,在握手发生后,记录被加密,并可以通过通信流发送。记录层的格式在 1.2 版本中增加了新字段,并在代码注释中用// 新字段标注如下:

    // Copyright (c) 2012 IETF Trust and the persons identified
       as authors of the code. All rights reserved.

    struct 
    {
      ContentType type;
      ProtocolVersion version;
      uint16 epoch;                                 // New field
      uint48 sequence_number;                       // New field
      uint16 length;
      opaque fragment[DTLSPlaintext.length];
    } DTLSPlaintext;

    struct 
    {
       ContentType type;
       ProtocolVersion version;
       uint16 epoch;                                 // New field
       uint48 sequence_number;                       // New field
       uint16 length;
       opaque fragment[DTLSCompressed.length];
    } DTLSCompressed;

    struct 
    {
       ContentType type;
       ProtocolVersion version;
       uint16 epoch;                                 // New field
       uint48 sequence_number;                       // New field
       uint16 length;
       select (CipherSpec.cipher_type) 
       {
          case block:  GenericBlockCipher;
          case aead:   GenericAEADCipher;             // New field
       } fragment;
    } DTLSCiphertext;

最后,以下是更新的握手协议:

    // Copyright (c) 2012 IETF Trust and the persons identified
       as authors of the code. All rights reserved.

    enum {
      hello_request(0), client_hello(1),
       server_hello(2),
      hello_verify_request(3),                       // New field
      certificate(11), server_key_exchange (12),
      certificate_request(13), server_hello_done(14),
      certificate_verify(15), client_key_exchange(16),
      finished(20), (255) } HandshakeType;

      struct {
        HandshakeType msg_type;
        uint24 length;
        uint16 message_seq;                            // New field
        uint24 fragment_offset;                        // New field
        uint24 fragment_length;                        // New field
        select (HandshakeType) {
          case hello_request: HelloRequest;
          case client_hello:  ClientHello;
          case server_hello:  ServerHello;
          case hello_verify_request: HelloVerifyRequest;  // New field
          case certificate:Certificate;
          case server_key_exchange: ServerKeyExchange;
          case certificate_request: CertificateRequest;
          case server_hello_done:ServerHelloDone;
          case certificate_verify:  CertificateVerify;
          case client_key_exchange: ClientKeyExchange;
          case finished: Finished;
        } body; } Handshake;

      struct {
        ProtocolVersion client_version;
        Random random;
        SessionID session_id;
        opaque cookie<0..2⁸-1>;                          // New field
        CipherSuite cipher_suites<2..2¹⁶-1>;
        CompressionMethod compression_methods<1..2⁸-1>; } ClientHello;

      struct {
        ProtocolVersion server_version;
        opaque cookie<0..2⁸-1>; } HelloVerifyRequest;

Java 9 中的 DTLS 支持

Java 9 对 DTLS API 的实现是传输无关且轻量级的。API 的设计考虑如下:

  • 读取超时将不会被管理

  • 实现将使用单个 TLS 记录进行每个封装/解封装操作

  • 应用程序,而不是 API,将需要:

    • 确定超时值

    • 组装乱序应用程序数据

DTLS 是一种在将数据传递到传输层协议之前,用于保护应用层数据的协议。DTLS 是加密和传输实时数据的好解决方案。应谨慎行事,以确保我们不会在我们的应用程序实现中引入漏洞。以下是在 Java 9 应用程序中实现 DTLS 的具体安全考虑:

  • 实现 DTLS v1.2,因为这是 Java 9 支持的最新版本。

  • 避免使用RSA加密。如果必须使用 RSA,请为您的私钥添加额外的安全性,因为这是 RSA 的弱点。

  • 使用 192 位或更多位时,请使用椭圆曲线迪菲-赫尔曼ECDH)匿名密钥协商协议。192 位值基于国家标准与技术研究院NIST)的建议。

  • 强烈推荐使用带关联数据的认证加密AEAD),这是一种加密形式。AEAD 为加密和解密的数据提供真实性、机密性和完整性保证。

  • 在实现握手重协商时,始终实现renegotiation_info扩展。

  • 在所有使用通信协议的 Java 应用程序中建立前向安全性FS)功能。实现 FS 确保当长期加密密钥受到损害时,过去的会话加密密钥不会受到损害。理想情况下,在需要传输数据最高安全性的 Java 应用程序中,将使用完美前向安全性PFS),其中每个密钥仅对单个会话有效。

创建 PKCS12 密钥存储

Java 9 平台为密钥存储提供了增强的安全性。为了欣赏 Java 增强提案 229 带来的变化,我们将默认创建 PKCS12 密钥存储,首先我们将回顾密钥存储的概念,查看KeyStore类,然后查看这些变化。

密钥存储简介

KeyStore的概念相对简单。它本质上是一个数据库文件,或者数据存储文件,用于存储公钥证书和私钥。Keystore将存储在/jre/lib/security/cacerts文件夹中。正如您将在下一节中看到的那样,这个数据库是由 Java 的java.security.KeyStore类方法管理的。

KeyStore功能包括:

  • 包含以下之一条目类型:

    • 私钥

    • 公钥证书

  • 每个条目都有唯一的别名字符串名称

  • 每个密钥的密码保护

Java 密钥存储(JKS)

java.security.KeyStore类是加密密钥和证书的存储设施。此类扩展java.lang.Object,如下所示:

    public class KeyStore extends Object

KeyStore管理三种类型的条目,每个条目都实现了KeyStore.Entry接口,这是KeyStore类提供的三个接口之一。条目实现定义在以下表中:

实现 描述
KeyStore.PrivateKeyEntry
  • 包含PrivateKey并可以以受保护格式存储它

  • 包含公钥的证书链

|

KeyStore.SecretKeyEntry
  • 包含SecretKey并可以以受保护格式存储它

|

KeyStore.TrustedCertifcateEntry
  • 包含来自外部源的单个公钥Certificate

|

此类自 Java 平台 1.2 版本以来就是其中的一部分。它有一个构造函数,三个接口,六个子类,以及几个方法。构造函数定义如下:

    protected KeyStore(KeyStoreSpi keyStoresSpi,
     Provider provider, String type)

KeyStore类包含以下接口:

  • public static interface KeyStore.Entry:

    • 此接口作为KeyStore条目类型的标记,不包含任何方法。
  • public static interface KeyStore.LoadStoreParameter:

    • 此接口作为加载和存储参数的标记,并具有以下返回 null 或用于保护KeyStore数据的参数的方法:

      • getProtectionParameter()
  • public static interface KeyStore.ProtectionParameter:

    • 此接口作为KeyStore保护参数的标记,不包含任何方法。

java.security.KeyStore类还包含以下列出的六个嵌套类。

Builder

当您想要延迟KeyStore的实例化时使用KeyStore.Builder类:

    public abstract static class KeyStore.Builder extends Object

此类提供了实例化KeyStore对象所需的信息。该类具有以下方法:

  • public abstract KeyStore getKeyStore() throws KeyStoreException

  • public abstract KeyStore.ProtectionParameter getProjectionParameter(String alias) throws KeyStoreException

  • newInstance的三种选项:

    • public static KeyStore.Builder newInstance(KeyStore keyStore, KeyStore.ProtectionParameter protectionParameter)

    • public static KeyStore.Builder newInstance(String type, Provider provider, File file, KeyStore.ProtectionParameter protection)

    • public static KeyStore.Builder newInstance(String type, Provider provider, KeyStore.ProtectionParameter protection)

CallbackHandlerProtection

KeyStore.CallbackHandlerProtection类的定义如下:

    public static class KeyStore.CallbackHandlerProtection extends
     Object implements KeyStore.ProtectionParameter

此类提供了一个ProtectionParameter来封装一个CallbackHandler,并具有以下方法:

    public CallbackHandler getCallbackHandler()

PasswordProtection

KeyStore.PasswordProtection类的定义如下:

    public static class KeyStore.PasswordProtection extends Object 
     implements KeyStore.ProtectionParameter, Destroyable

此调用提供了一个基于密码的ProtectionParameter实现。该类具有以下方法:

  • public void destroy() throws DestroyFailedException:

    • 此方法清除密码
  • public char[] getPassword():

    • 返回密码的引用
  • public boolean isDestroyed():

    • 如果密码已被清除,则返回 true

PrivateKeyEntry

KeyStore.PrivateKeyEntry类的定义如下:

    public static final class KeyStore.PrivateKeyEntry extends
     Object implements KeyStore.Entry

这创建了一个条目来保存PrivateKey及其对应的Certificate链。此类具有以下方法:

  • public Certificate getCertificate():

    • Certificate链中返回端实体Certificate
  • public Certificate[] getCertificateChain():

    • 返回Certificate链作为Certificates数组
  • public PrivateKey getPrivateKey():

    • 返回当前条目的PrivateKey
  • public String toString():

    • 返回PrivateKeyEntry作为String

SecretKeyEntry

KeyStore.SecretKeyEntry类的定义如下:

    public static final class KeyStore.SecretKeyEntry extends
     Object implements KeyStore.Entry

此类包含一个SecretKey并具有以下方法:

  • public SecretKey getSecretKey():

    • 返回条目的SecretKey
  • public String toString():

    • 返回SecretKeyEntry作为String

TrustedCertificateEntry

KeyStore.TrustedCertificateEntry类的定义如下:

    public static final class KeyStore.TrustedCertificateEntry extends
     Object implements KeyStore.Entry

此类包含一个受信任的Certificate,并具有以下方法:

  • public Certificate getTrustedCertificate():

    • 返回条目的受信任Certificate
  • public String toString():

    • 返回条目的受信任Certificate作为String

使用此类的关键是理解流程。首先,我们必须使用getInstance方法加载KeyStore。接下来,我们请求访问KeyStore实例。然后,我们可以读取和写入Object

以下代码片段显示了加载-请求-访问实现:

    . . . 

    try {
      // KeyStore implementation will be returned for the default type
      KeyStore myKS = KeyStore.getInstance(KeyStore.getDefaultType());

      // Load
      myKS.load(null, null);

      // Instantiate a KeyStore that holds a trusted certificate
      TrustedCertificateEntry myCertEntry =
        new TrustedCertificateEntry(generateCertificate());

      // Assigns the trusted certificate to the "pack.pub" alias
      myKS.setCertificateEntry("packt.pub",
       myCertEntry.getTrustedCertificate());

      return myKS;
    } 
    catch (Exception e) {
      throw new AssertionError(e);
    }
  }
  . . .

Java 9 中的 PKCS12 默认设置

在 Java 9 之前,默认的KeyStore类型是Java 密钥库JKS)。Java 9 平台现在使用 PKCS 作为默认的KeyStore类型,更具体地说,是 PKCS12。

PKCS公钥加密标准的缩写。

与 JKS 相比,此 PKCS 更改提供了更强的加密算法。正如你所期望的,JDK 9 仍然与 JKS 兼容,以支持先前开发的系统。

提高安全应用性能

Java 增强提案 232,标题为提高安全应用性能,专注于在安装了安全管理器的应用程序运行时的性能改进。安全管理者可能导致处理开销和低于理想的应用程序性能。

这是一项令人印象深刻的任务,因为当前在运行安全管理者时的 CPU 开销估计会导致 10-15%的性能下降。完全移除 CPU 开销是不切实际的,因为运行安全管理者需要一些 CPU 处理。尽管如此,本提案(JEP-232)的意图是尽可能减少开销百分比。

这项工作导致了以下优化,每个优化将在后续章节中详细说明:

  • 安全策略强制执行

  • 权限评估

  • 哈希码

  • 包检查算法

安全策略强制执行

JDK 9 使用ConcurrentHashMapProtectionDomain映射到PermissionCollectionConcurrentHashMap通常用于应用程序中的高并发。它具有以下特性:

  • 线程安全

  • 进入映射不需要同步

  • 快速读取

  • 写入使用锁

  • 无对象级别锁定

  • 在非常细粒度的级别上锁定

ConcurrentHashMap类定义如下:

    public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> 
     implements ConcurrentMap<K, V>, Serializable

在前面的类定义中,K指的是哈希图维护的键的类型,而V表示映射值的类型。有一个KeySetView子类和几个方法。

与强制执行安全策略相关的有三个额外的类--ProtectionDomainPermissionCollectionSecureClassLoader

  • ProtectionDomain类用于封装一组类,以便可以授予域权限。

  • PermissionCollection类表示一组权限对象。

  • SecureClassLoader类扩展了ClassLoader类,为定义具有系统策略检索权限的类提供了额外的功能。在 Java 9 中,此类使用ConcurrentHashMap以增加安全性。

权限评估

在权限评估的类别下,进行了三项优化:

  • identifyPolicyEntries 列表之前有用于同步的策略提供者代码。在 JDK 9 中已删除此代码。

  • PermissionCollection 条目现在存储在 ConcurrentHashMap 中。它们之前在 Permission 类中作为 HashMap 存储。

  • 权限现在存储在 PermissionCollection 子类的并发集合中。

java.Security.CodeSource

哈希码是一个对象生成的数字,存储在哈希表中以实现快速存储和检索。Java 中的每个对象都有一个哈希码。以下是哈希码的一些特性和规则:

  • 在运行进程中的相等对象具有相同的哈希码

  • 哈希码可能在执行周期之间发生变化

  • 不应将哈希码用作键

Java 9 平台包括对 java.security.CodeSourcehashCode 方法的修改,以优化 DNS 查询。这些操作可能会很耗时,因此使用代码源 URL 的字符串版本来计算哈希码。

CodeSource 类的定义如下:

    public class CodeSource extends Object implements Serializable

此类有以下方法:

  • public boolean equals(Object obj): 如果对象相等,则返回 true。这覆盖了 Object 类中的 equals 方法。

  • public final Certificate[] getCertificates(): 返回证书数组。

  • public final CodeSigner[] getCodeSigners(): 返回与 CodeSource 关联的代码签名者的数组。

  • public final URL getLocation(): 返回 URL。

  • public int hashCode(): 返回当前对象的哈希码值。

  • public boolean implies(CodeSource codesource): 如果给定的代码源满足以下标准,则返回 true

    • 不为空

    • 对象的证书不为空

    • 对象的位置不为空

  • public String toString(): 返回包含 CodeSource 信息的 String,包括位置和证书。

包检查算法

当运行带有安全策略管理器的应用程序时,Java 9 的最终性能改进以 java.lang.SecurityManager 包增强的形式出现。具体来说,修改了 checkPackageAccess 方法的包检查算法。

java.lang.SecurityManager 类允许应用程序在特定操作上实现安全策略。该类的 public void checkPackageAccess(String pkg) 方法从 getProperty 方法接收一个逗号分隔的受限包列表。如图所示,根据评估结果,checkPackageAccess 方法可能会抛出两种异常之一:

图片

TLS 应用层协议协商扩展

Java Enhancement Proposal 244 简单地增强了 javax.net.ssl 包,使其支持 传输层安全性TLS应用层协议协商ALPN)扩展。此扩展允许 TLS 连接进行应用协议协商。

TLS ALPN 扩展

ALPN 是 TLS 扩展,可用于在安全连接中使用时协商要实现的协议。ALPN 代表了一种高效的协议协商方式。如图所示,TLS 握手有五个基本步骤:

javax.net.ssl 包

java.net.ssl包包含与安全套接字包相关的类。这使我们能够使用 SSL 作为示例,以可靠地检测网络字节流中引入的错误。它还提供了加密数据以及提供客户端和服务器身份验证的能力。

此包包括以下接口:

  • public interface HandshakeCompletedListener extends EventListener

  • public interface HostnameVerifier

  • public interface KeyManager

  • `public interface ManagerFactoryParameters`

  • public interface SSLSession

  • public interface SSLSessionBindingListener extends EventListener

  • public interface SSLSessionContext

  • public interace TrustManager

  • public interface X509KeyManager extends KeyManager

  • public interface X509TrustManager extends TrustManager

java.net.ssl包还有以下子类:

  • public class CertPathTrustManagerParameters extends Object implements ManagerFactoryParameters

  • public abstract class ExtendedSSLSession extends Object implements SSLSession

  • public class HandshakeCompleteEvent extends EventObject

  • public abstract class HttpsURLConnection extends HttpURLConnection

  • public class KeyManagerFactory extends Object

  • public abstract class KeyManagerFactorySpi

  • public class KeyStoreBuilderParameters extends Object implements ManagerFactoryParameters

  • public class SSLContext extends Object

  • public abstract class SSLContextSpi extends Object

  • public abstract class SSLEngine extends Object

  • public class SSLEngineResult extends Object

  • public class SSLParameters extends Object

  • public final class SSLPermission extends BasicPermission

  • public abstract class SSLServerSocket extends ServerSocket

  • public abstract class SSLServerSocketFactory extends ServerSocketFactory

  • public class SSLSessionBindingEvent extends EventObject

  • public abstract class SSLSocket extends Socket

  • public abstract class SSLSocketFactory extends SocketFactory

  • public class TrustManagerFactory extends Object

  • `public abstract class TrustManagerFactorySpi extends Object`

  • public abstract class X509ExtendedKeyManager extends Object implements X509KeyManager

  • public abstract class X509ExtendedTrustManager extends Object implements x509TrustManager

java.net.ssl 包扩展

Java 9 平台中java.net.ssl包的改变是现在它支持 TLS ALPN 扩展。这个改变的益处包括:

  • TLS 客户端和服务器现在可以使用多个应用层协议,这些协议可能使用也可能不使用相同的传输层端口

  • ALPN 扩展允许客户端优先选择它支持的应用层协议

  • 服务器可以选择客户端协议和 TLS 连接

  • 支持 HTTP/2

以下插图之前已作为 TLS 握手的五个基本步骤展示。更新为 Java 9 并在此处展示,该插图表明客户端和服务器之间共享了协议名称:

一旦收到客户端的应用层协议列表,服务器可以选择服务器首选的交集值,并外部扫描初始明文 ClientHellos 并选择一个 ALPN 协议。应用程序服务器将执行以下操作之一:

  • 选择任何支持的协议

  • 判断 ALPN 值(远程提供和本地支持)是互斥的

  • 忽略 ALPN 扩展

与 ALPN 扩展相关的其他关键行为:

  • 服务器可以更改连接参数

  • SSL/TLS 握手开始后,应用程序可以查询是否已选择 ALPN 值

  • SSL/TLS 握手结束后,应用程序可以回顾使用了哪个协议

ClientHello 是 TLS 握手的第一个消息。它具有以下结构:

    struct {
      ProtocolVersion client_version;
      Random random;
      SessionID session_id;
      CipherSuite cipher_suites<2..2¹⁶-1>;
      CompressionMethod compression_methods<1..2⁸-1>;
      Extension extensions<0..2¹⁶-1>;
    } ClientHello;

利用 CPU 指令进行 GHASH 和 RSA

Java 增强提案(JEP)246 的自描述标题 Leverage CPU Instructions for GHASH and RSA,对其目标提供了深刻的见解。此 JEP 的目的是提高加密操作的性能,特别是 GHASH 和 RSA。Java 9 通过利用最新的 SPARC 和 Intel x64 CPU 指令实现了性能提升。

此增强功能不需要在 Java 9 平台上添加或修改新的或修改后的 API。

散列

Galois HASHGHASH)和 Rivest-Shamir-AdlemanRSA)是加密系统散列算法。散列是从文本字符串生成的固定长度字符串或数字。算法,特别是散列算法,被设计成结果散列无法逆向工程。我们使用散列存储带有盐生成的密码。

在密码学中,盐是作为散列函数输入的随机数据,用于生成密码。盐有助于防止彩虹表攻击和字典攻击。

以下图形说明了散列工作的基本原理:

如您所见,散列算法将明文和盐输入其中,生成新的散列密码和盐,并将盐存储起来。以下是带有示例输入/输出的相同图形,以演示其功能:

验证过程,以下图表从用户输入其明文密码开始。散列算法将明文与存储的盐重新散列。然后,将生成的散列密码与存储的密码进行比较:

TLS 的 OCSP stapling

在线证书状态协议 (OCSP) 钩钉是一种检查数字证书撤销状态的方法。评估 SSL 证书有效性的 OCSP 钩钉方法被认为是既安全又快捷的。通过允许 Web 服务器提供其有机证书的有效性信息,而不是从证书发行商请求验证信息的更长时间过程,实现了确定速度。

在线证书状态协议 (OCSP) 钩钉之前被称为 传输层安全性 (TLS) 证书状态请求扩展。

OCSP 钩钉入门

OCSP 钩钉过程涉及多个组件和有效性检查。以下图形说明了 OCSP 钩钉过程:

图片

如您所见,过程始于用户尝试通过浏览器打开 SSL 加密的网站。浏览器查询 Web 服务器以确保 SSL 加密的网站具有有效的证书。Web 服务器查询证书的供应商,并提供了证书状态和数字签名的时戳。Web 服务器将这两个组件(证书状态和数字签名的时戳)组合在一起,并将其返回给请求的浏览器。然后,浏览器可以检查时戳的有效性,并决定是否显示 SSL 加密的网站或显示错误。

Java 9 平台更改

Java 增强提案 249,TLS 的 OCSP 钩钉,通过 TLS 证书状态请求扩展实现 OCSP 钩钉。OCSP 钩钉检查 X.509 证书的有效性。

X.509 证书是使用 X509 公钥基础设施 (PKI) 的数字证书。

在 Java 9 之前,证书有效性检查(实际上,是检查证书是否已被撤销)可以在客户端启用,并且具有以下低效性:

  • OCSP 响应者性能瓶颈

  • 多次传递导致的性能下降

  • 如果在客户端执行 OCSP 检查,则会导致额外的性能下降

  • 当浏览器未连接到 OCSP 响应者时,会出现错误的 失败

  • OCSP 响应者遭受拒绝服务攻击的易感性

新的 TLS OCSP 钩钉包括以下针对 Java 9 平台的系统属性更改:

  • jdk.tls.client.enableStatusRequestExtension:

    • 默认设置:true

    • 启用 status_request 扩展

    • 启用 status_request_v2 扩展

    • 启用从服务器处理 CertificateStatus 消息

  • jdk.tls.server.enableStatusRequestExtension:

    • 默认设置:false

    • 启用服务器端 OCSP 钩钉支持

  • jdk.tls.stapling.responseTimeout:

    • 默认设置:5000 毫秒

    • 控制服务器分配的最大时间以获取 OCSP 响应

  • jdk.tls.stapling.cacheSize:

    • 默认设置:256

    • 控制缓存条目的最大数量

    • 可以将最大值设置为零以消除上限

  • jdk.tls.stapling.cacheLifetime:

    • 默认设置:3600 秒(1 小时)

    • 控制缓存响应的最大生存期

    • 可以将值设置为零以禁用缓存生存期

  • jdk.tls.stapling.responderURI:

    • 默认设置:无

    • 可以设置默认 URI 以证书不包含 Authority Info AccessAIA)扩展

    • 除非设置 jdk.tls.stapling.Override 属性,否则不会覆盖 AIA 扩展

  • jdk.tls.stapling.respoderOverride:

    • 默认设置:false

    • 允许通过 jdk.tls.stapling.responderURI 提供的属性覆盖 AIA 扩展值

  • jdk.tls.stapling.ignoreExtensions:

    • 默认设置:false

    • 禁用 status_requeststatus_request_v2 TLS 扩展中指定的 OCSP 扩展转发。

status_requeststatus_request_v2 TLS 欢迎扩展现在由客户端和服务器端 Java 实现支持。

基于 DRBG 的 SecureRandom 实现

在 Java 9 之前,JDK 有两种生成安全随机数的方法。一种方法是用 Java 编写的,基于 SHA1 的随机数生成,并不十分强大。另一种方法是平台相关的,并使用预配置的库。

确定性随机位生成器DRBG)是生成随机数的方法。它已获得美国商务部下属的 国家标准与技术研究院NIST)的批准。DRBG 方法包括用于生成安全随机数的现代和更强的算法。

Java 增强提案 273,基于 DRBG 的 SecureRandom 实现旨在实现三种特定的 DRBG 机制。这些机制如下列出:

  • Hash_DRBG

  • HMAC_DRBG

  • CTR_DRBG

您可以在 nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf 上了解每个 DRBG 机制的具体信息

这里是三个新的 API:

  • SecureRandom: 新方法允许使用以下列出的可配置属性配置 SecureRandom 对象:

    • 播种

    • 重新播种

    • 随机位生成

  • SecureRandomSpi: 实现新的 SecureRandom 方法

  • SecureRandomParameter: 新接口,以便将输入传递到新的 SecureRandom 方法

摘要

在本章中,我们探讨了 JDK 中涉及安全性的几个小但重要的变化。Java 9 平台中的特色安全增强功能为开发者提供了编写和维护实现安全性的应用程序的独特能力。具体来说,我们涵盖了 DTLS、密钥库、提高安全应用程序性能、TLS 应用层协议协商扩展、利用 CPU 指令进行 GHASH 和 RSA、TLS 的 OCSP 撕贴以及基于 DRBG 的 SecureRandom 实现。

在下一章中,我们将探讨 Java 9 中使用的新的命令行标志以及各种命令行工具的变更。我们的内容将包括使用新的命令行选项和标志来管理 Java JVM 运行时和编译器。

第十四章:命令行标志

在上一章中,我们探讨了 JDK 的几个安全变化。Java 9 的安全增强为开发者提供了编写和维护实现安全的应用程序的能力。具体来说,我们涵盖了数据报传输层安全、密钥库、提高安全应用性能、TLS 应用层协议协商扩展、利用 CPU 指令进行 GHASH 和 RSA、TLS 的 OCSP stapling 以及基于 DRBG 的SecureRandom实现。

在本章中,我们将探讨 Java 9 平台的一些变化,这些变化的共同主题是命令行标志。具体来说,我们将涵盖以下概念:

  • 统一 JVM 日志

  • 编译器控制

  • 诊断命令

  • 堆分析代理

  • 移除你的 JHAT

  • 命令行标志参数验证

  • 为旧平台版本编译

统一 JVM 日志 [JEP 158]

为 JVM 创建一个统一的日志架构是 JEP-158 的核心目标。以下是 JEP 目标的综合列表:

  • 为所有日志操作创建一个 JVM 范围的命令行选项集

  • 使用分类标签进行日志记录

  • 允许消息具有多个标签,也称为标签集

  • 提供六个日志级别:

    • 错误

    • 警告

    • 信息

    • 调试

    • 跟踪

    • 开发

  • 根据级别选择要记录的消息

  • 可选地将日志直接输出到控制台或文件

    • 每次打印一行,并且不支持同一行内的交错
  • 允许输出多行日志(非交错)

  • 格式化所有日志消息,以便它们易于人类阅读

  • 添加如运行时间、级别和标签等装饰

  • 与级别一样,根据装饰选择要记录的消息

  • 将 Java 9 之前的tty>print日志转换为使用统一日志作为输出

  • 允许使用jcmdMBeans动态配置消息

  • 允许启用和禁用单个日志消息

  • 添加确定装饰打印顺序的能力

统一日志对 JVM 的更改可以归纳为以下五个类别:

  • 命令行选项

  • 装饰

  • 级别

  • 输出

  • 标签

让我们简要地看看这些类别。

命令行选项

新的命令行选项-Xlog被添加到 Java 9 的日志框架中。这个命令行选项有一系列参数和可能性。基本语法是简单地-Xlog后跟一个选项。以下是正式的基本语法:

-Xlog[:option]

这里有一个使用all选项的基本示例:

-Xlog:all

这里是配置新统一日志的广泛命令行语法:

-Xlog[:option]

option          := [<what>][:[<output>][:[<decorators>][:<output-options>]]]
 'help'
 'disable'

what            := <selector>[,...]

selector        := <tag-set>[*][=<level>]

tag-set         := <tag>[+..]
 'all'

tag             := name of tag

level           := trace
 debug
 info
 warning
 error

output          := 'stderr'
 'stdout'
 [file=]<filename>

decorators      := <decorator>[,...]
 'none'

decorator       := time
 uptime
 timemillis
 uptimemillis
 timenanos
 uptimenanos
 pid
 tid
 level
 tags

output-options  := <output_option>[,...]

output-option   := filecount=<file count>
 filesize=<file size in kb>
 parameter=value

以下-Xlog示例后面跟着一个描述:

-Xlog:all

在前面的示例中,我们正在告诉 JVM 执行以下操作:

  • 记录所有消息

  • 使用info级别

  • 提供输出到stdout

通过这个示例,所有warning消息仍然会输出到stderr

下一个示例,在此处展示,记录了debug级别的消息:

-Xlog:gc+rt*=debug

在前面的示例中,我们正在告诉 JVM 执行以下操作:

  • 记录所有带有至少gcrt标签的消息

  • 使用debug级别

  • 提供输出到stdout

以下示例将输出推送到外部文件:

-Xlog:disable - Xlog:rt=debug:rtdebug.txt

在前面的示例中,我们正在告诉 JVM 执行以下操作:

  • 禁用所有消息,除了带有rt标签的消息

  • 使用debug级别

  • 提供输出到名为rtdebug.txt的文件

Decorations

在 Java 9 的日志框架的上下文中,装饰是关于日志消息的元数据。以下是可用的装饰字母列表:

  • level:与记录的消息关联的级别

  • pid:PID = 处理器标识符

  • tags:与记录的消息关联的标签集

  • tid:TID = 线程标识符

  • time:使用 ISO-8601 格式引用当前日期和时间

  • timemillis:当前时间(以毫秒为单位)

  • timenanos:当前时间(以纳秒为单位)

  • uptime:自 JVM 启动以来的时间(以秒和毫秒为单位)

  • uptimemillis:自 JVM 启动以来的时间(以毫秒为单位)

  • uptimenanos:自 JVM 启动以来的时间(以纳秒为单位)

装饰可以被超越或包含在统一的日志输出中。无论使用哪种装饰,它们都会按照以下顺序出现在输出中:

  1. time

  2. uptime

  3. timemillis

  4. uptimemillis

  5. timenanos

  6. uptimenanos

  7. pid

  8. tid

  9. level

  10. tags

Levels

记录的消息与一个详细的级别相关联。如前所述,级别包括错误警告信息调试跟踪开发。以下图表显示了级别如何根据记录的信息量具有递增的详细程度。开发级别仅用于开发目的,并且在产品应用构建中不可用:

Output

Java 9 日志框架支持三种类型的输出,以下是用-Xlog命令行语法直接使用的示例:

在以下示例中,我们提供输出到stderr

-Xlog:all=warning:stderr:none

以下示例将输出提供给stdout

-Xlog:all=warning:stdout:none

以下示例将输出写入文本文件:

-Xlog:all=warning:file=logmessages.txt:none

Tags

新的日志框架由 JVM 中标识的一组标签组成。如果需要,可以在源代码中更改这些标签。标签应该是自我标识的,例如gc代表垃圾回收。

当多个标签组合在一起时,它们形成一个标签集。当我们通过源代码添加自己的标签时,每个标签都应该与一个标签集相关联。这将有助于确保标签保持组织并易于人类阅读。

Compiler control [JEP 165]

控制 Java 虚拟机编译器可能看起来是一项不必要的任务,但对于许多开发者来说,这是测试的重要方面。Java 增强提案 165 详细描述了实施 JVM 编译器运行时管理的计划。这是通过方法相关的编译器标志实现的。

在本节中,我们将从 JVM 编译模式开始介绍,然后探讨可以使用 Java 9 平台控制的编译器。

编译模式

Java 9 平台的变化包括对 c1 和 c2 JVM 编译器的细粒度控制。正如您在以下插图中所见,Java HotSpot JVM 有两个即时JIT)编译模式--c1c2

C1C2编译模式使用不同的编译技术,如果用于相同的代码库,可以产生不同的机器代码集。

C1 编译模式

Java HotSpot VM 内部的 C1 编译模式通常用于具有以下特性的应用程序:

  • 快速启动

  • 增加优化

  • 客户端

C2 编译模式

第二种编译模式,C2,被具有以下列出的特性的应用程序使用:

  • 长运行时间

  • 服务器端

分层编译

分层编译允许我们使用c1c2编译模式。从 Java 8 开始,分层编译是默认过程。如图所示,c1模式在启动时使用,以帮助提供更大的优化。然后,一旦应用程序足够热身,就采用c2模式:

Java 9 中的编译器控制

Java 9 承诺能够对 JVM 编译器进行有限的控制,并在运行时进行更改。这些额外的功能不会降低性能。这允许进行更精确的测试和测试优化,因为我们可以在不重新启动整个 JVM 的情况下运行小的编译器测试。

为了控制编译器操作,我们需要创建一个指令文件。这些文件包含编译器指令,由一组带有值的选项组成。指令文件本质上使用 JSON 的一个子集:

JavaScript 对象表示法JSON)格式用于数据交换。指令文件与 JSON 有以下格式差异:

  • intdoubles是唯一支持的数字格式

  • 双斜杠(//)可用于注释行

  • 数组和对象中可以使用尾随逗号(,

  • 转义字符不受支持

  • 选项名称格式化为字符串,不需要引用

您可以在www.json.org了解更多关于 JSON 的信息。

我们可以在命令行使用以下语法添加我们的指令文件:

-XX:CompilerDirectivesFile=<file>

这里是一个指令文件的 shell 示例:

    [  // Open square bracket marks the start of the directives file

    { // Open curly brace marks the start of a directive block

      // A directives block that applies specifically to the C1 mode
      c1: {   
             // directives go here
          },

      // A directives block that applies specifically to the C2 mode
      c2: {   
             // directives go here
          },

      // Here we can put a directives that do not apply to
      // a specific compiler mode

    },

    {  // can have multiple directive blocks

       c1: {
             // directives go here
           }   

       c2: {
             // directives go here
           }    
    }

    ] // Close square bracket marks the start of the directives file

诊断命令 [JEP 228]

Java 增强提案 228,添加更多诊断命令,定义了七个额外的诊断命令,以增强诊断 JDK 和 JVM 的能力。新的诊断命令在此详细说明。

print_codegenlist命令打印当前排队编译的方法。由于 c1 和 c2 编译模式在不同的队列上,因此需要向特定队列发出此命令。

dump_codelist诊断命令将打印以下列出的编译方法信息:

  • 完整签名

  • 地址范围

  • 状态

    • 活着

    • 非侵入式

    • 僵尸

此外,dump_codelist 诊断命令允许将输出定向到 stdout 或指定的文件。输出可以是 XML 格式或标准文本。

print_codeblocks 命令允许我们打印:

  • 代码缓存大小

  • 代码缓存列表

  • 代码缓存中的块列表

  • 代码块的地址

Th datadump_request 诊断命令向 Java 虚拟机工具接口JVMTI)发送转储请求。这取代了 Java 虚拟机调试接口JVMDI)和 Java 虚拟机分析接口JVMPI)接口。

使用 set_vmflag 命令,我们可以在 JVM 或库中设置命令行标志或选项。

Th print_class_summary 诊断命令打印所有已加载类的列表以及它们的继承结构。

print_utf8pool 命令打印所有 UTF-8 字符串常量。

堆分析代理 [JEP 240]

Java 增强提案 240 的标题为 移除 JVM TI hprof 代理。以下是与此 JEP 相关并可能在标题中引用的术语,可能对您来说是新的:

  • 工具接口(TI):这是一个本地编程接口,允许工具控制运行在 Java 虚拟机内部的应用程序的执行。该接口还允许状态查询。此工具的完整名称是 Java 虚拟机工具接口,或 JVM TI。

  • 堆分析(HPROF):这是一个内部 JDK 工具,用于分析 JVM 对 CPU 和堆的使用。开发者最常接触到的 hprof 是在崩溃后生成的文件。生成的文件包含堆转储。

Java 9 JDK 不包含 hprof 代理。它被移除主要是因为有更优越的替代方案。以下是相关功能表的列表:

HPROF 功能 替代方案
分配分析器(heap=sites) Java VisualVM
CPU 分析器(cpu=samples)(cpu=times) Java VisualVMJava Flight Recorder

| 堆转储(heap=dump) | 内部 JVM 功能:

  • GC.heap_dump(icmd <pid> GC.heap_dump)

  • jmap -dump

|

有趣的是,当 HPROF 最初创建时,它并不是打算在生产环境中使用。事实上,它只是为了测试 JVM 工具接口的代码。因此,随着 Java 9 平台的出现,HPROF 库(libhprof.so)将不再包含在 JDK 中。

移除您的 JHAT [JEP 241]

Java 堆分析工具JHAT)用于解析 Java 堆转储文件。此堆转储文件解析工具的语法如下:

jhat 
     [-stack <bool>] 
     [-refs <bool>] 
     [-port <port>] 
     [-baseline <file>] 
     [-debug <int>] 
     [-version] 
     [-h|-help] 
     <file>

这里快速查看与 JHAT 命令相关的选项:

选项 描述 默认
-J<flag> <flag> 传递给运行时系统。 N/A
-stack<bool> 切换对对象分配调用堆栈的跟踪。 true
-refs<bool> 切换对对象的引用跟踪。 true
-port<port> 指示 JHAT HTTP 服务器的端口。 7000
-exclude<exclude-filename> 从可达对象查询中排除指定的文件。 N/A
-baseline<filename> 指定用于比较的基线堆转储。 N/A
-debug<int> 设置输出详细程度。 N/A
-version 简单地输出 JHAT 发布号。 N/A
-h -help 提供帮助文本。 N/A

JHAT 自 JDK-6 以来一直是 Java 平台的一部分,以实验形式存在。它没有得到支持,并被认为已经过时。从 Java 9 开始,这个工具将不再包含在 JDK 中。

JVM 命令行标志参数验证[JEP 245]

在本章中,您已经接触到了 Java 9 平台的大部分命令行标志用法。Java 增强提案 245,标题为验证 JVM 命令行标志参数,旨在确保所有带有参数的 JVM 命令行标志都得到验证。这项工作的主要目标是:

  • 避免 JVM 崩溃

  • 提供错误消息以通知无效的标志参数

如以下图形所示,没有尝试自动纠正标志参数错误;而是仅仅识别错误并防止 JVM 崩溃:

图片

这里提供了一个示例错误消息,表明标志参数超出了范围。这个错误会在 JVM 初始化期间执行的标志参数范围检查期间显示:

exampleFlag UnguardOnExecutionViolation = 4 is outside the allowed range [ 0 . . . 3]

关于这个对 Java 平台的更改,以下是一些具体细节:

  • 扩展当前的globals.hpp源文件以确保完整的标志默认值和允许的范围得到文档记录

  • 定义一个框架以支持未来添加新的 JVM 命令行标志:

    • 这将包括值范围和值集

    • 这将确保有效性检查将适用于所有新添加的命令行标志

  • 修改宏表:

    • 为可选范围添加最小/最大值

    • 为以下内容添加约束条目:

      • 确保每次标志更改时都执行约束检查

      • 当 JVM 运行时,所有可管理的标志将继续被检查

为旧平台版本编译[JEP 247]

Java 编译器javac在 Java 9 中进行了更新,以确保它可以用来编译 Java 程序以在用户选择的旧版本的 Java 平台上运行。这是 Java 增强提案 247,为旧平台版本编译的焦点。

如以下截图所示,javac有几个选项,包括-source-target。以下截图中的javac来自 Java 8:

图片

-source选项用于指定编译器接受的 Java 版本。-target选项通知javac将生成哪个版本的类文件。默认情况下,javac生成与最新 java 版本和平台 API 相对应的类文件。当编译的应用程序使用仅在最新平台版本中可用的 API 时,这可能会导致问题。这会使应用程序在旧平台版本上无法运行,尽管-source-target选项已指定。

为了解决上述问题,Java 9 平台引入了一个新的命令行选项。这个选项是--release选项,当使用时,将自动配置 javac 以生成链接到特定平台版本的类文件。以下截图显示了 Java 9 平台的javac选项。如您所见,新的--release选项已被包括在内:

下面是新选项的语法:

javac --release <release> <source files>

摘要

在本章中,我们探讨了 Java 9 平台的一些变化,主题是命令行标志。具体来说,我们涵盖了统一的 JVM 日志记录、编译器控制、新的诊断命令、移除 HPROF 堆分析代理、移除 JHAT、命令行标志参数验证以及为旧平台版本编译的能力。

在下一章中,我们将关注 Java 9 平台提供的附加实用工具的最佳实践。这些将包括 UTF-8、Unicode 7.0、Linux 等。

第十五章:最佳实践在 Java 9 中

在上一章中,我们探讨了 Java 9 中有关命令行标志的几个更改。具体来说,我们涵盖了统一的 JVM 日志、编译器控制、新的诊断命令、移除 HPROF 堆分析代理、移除Java 堆分析工具JHAT)、命令行标志参数验证以及为旧平台版本编译的能力。

在本章中,我们将关注 Java 9 平台提供的附加实用程序的最佳实践。具体来说,我们将涵盖:

  • 支持 UTF-8

  • Unicode 7.0.0

  • Linux/AArch64 端口

  • 多分辨率图像

  • 常见区域数据存储库

支持 UTF-8

Unicode 转换格式-8UTF-8)是一个字符集,它使用一到四个 8 位字节封装所有 Unicode 字符。它是 Unicode 的字节编码形式。UTF-8 自 2009 年以来一直是编码网页的主要字符集。以下是 UTF-8 的一些特性:

  • 可以编码所有 1,112,064 个 Unicode 代码点

  • 使用一到四个 8 位字节

  • 占据了几乎所有网页的近 90%

  • 与 ASCII 向后兼容

  • 可逆

普遍使用 UTF-8 强调了确保 Java 平台完全支持 UTF-8 的重要性。这种思维方式导致了 Java 增强提案 226,UTF-8 属性资源包。在 Java 9 应用程序中,我们有指定具有 UTF-8 编码的属性文件的能力。Java 9 平台包括对ResourceBundle API 的更改以支持 UTF-8。

让我们看看 Java 9 之前的ResourceBundle类,然后是 Java 9 平台对该类所做的更改。

ResourceBundle

以下类为开发者提供了从资源包中隔离特定区域资源的能力。这个类显著简化了本地化和翻译:

    public abstract class ResourceBundle extends Object

创建资源包需要一种有目的的方法。例如,让我们想象我们正在创建一个将支持商业应用程序的多种语言的资源包。我们的按钮标签,以及其他事项,将根据当前的区域设置显示不同。因此,在我们的例子中,我们可以为我们的按钮创建一个资源包。我们可以称它为buttonResources。然后,对于每个区域设置,我们可以创建一个buttonResource_<identifier>。以下是一些示例:

  • buttonResource_ja:用于日语

  • buttonResource_uk:用于英国英语

  • buttonResource_it:用于意大利语

  • buttonResource_lh:用于立陶宛语

我们可以使用与我们的基本包相同的名称的资源包。因此,buttonResource将包含我们的默认包。

要获取特定区域的对象,我们调用getBundle方法。以下是一个示例:

    . . . 

    ResourceBundle = buttonResource =
     ResourceBundle.getBundle("buttonResource", currentLocale);

    . . . 

在接下来的几节中,我们将通过查看其嵌套类、字段和构造函数以及包含的方法来检查ResourceBundle类。

嵌套类

ResourceBundle 类相关联的一个嵌套类是 ResourceBundle.Control 类。它提供了当使用 ResourceBundle.getBundle 方法时使用的回调方法:

    public static class ResourceBundle.Control extends Object

ResourceBundle.Control 类有以下字段:

  • public static final List<String> FORMAT_CLASS

  • public static final List<String> FORMAT_DEFAULT

  • public static final List<String> FORMAT_PROPERTIES

  • public static final long TTL_DONT_CACHE

  • public static final long TTL_NO_EXPIRATION_CONTROL

该类有一个单独的空构造函数和以下方法:

  • getCandidateLocales():
      public List<Locale> getCandidateLocales(String baseName,
       Locale locale)
组件 详细信息
抛出 NullPointerException(如果 baseNamelocale 为 null)
参数 baseName: 一个完全限定的类名locale: 期望的区域设置
返回 候选区域设置列表
  • getControl():
      public static final ResourceBundle.Control getControl(
       List<String> formats)
组件 详细信息
抛出 IllegalArgumentException(如果 formats 未知)NullPointerException(如果 formats 为 null)
参数 formats: 这些是 ResourceBundle.Control.getFormats 方法将返回的格式
返回 支持指定格式的 ResourceBundle.Control
  • getFallbackLocale():
      public Locale getFallbackLocale(String baseName, Locale locale)
组件 详细信息
抛出 NullPointerException(如果 baseNamelocale 为 null)
参数 baseName: 一个完全限定的类名locale: 使用 ResourceBundle.getBundle 方法无法找到的期望区域设置
返回 回退区域设置
  • getFormats():
      public List<String> getFormats(String baseName)
组件 详细信息
抛出 NullPointerException(如果 baseName 为 null)
参数 baseName: 一个完全限定的类名
返回 包含其格式的字符串列表,以便可以加载资源包
  • getNoFallbackControl():
      public static final ResourceBundle.Control   
      getNoFallbackControl(List<String> formats)
组件 详细信息
抛出 IllegalArgumentException(如果 formats 未知)NullPointerException(如果 formats 为 null)
参数 formats: 这些是 ResourceBundle.Control.getFormats 方法将返回的格式
返回 支持指定格式且没有回退区域设置的 ResourceBundle.Control
  • getTimeToLive():
      public long getTimeToLive(String baseName, Locale locale)
组件 详细信息
抛出 NullPointerException(如果 baseName 为 null)
参数 baseName: 一个完全限定的类名locale: 期望的区域设置
返回 从缓存时间偏移的零或正毫秒数
  • needsReload():
      public boolean needsReload(String baseName, Locale locale,
       String format, ClassLoader loader, ResourceBundle bundle,
       long loadTime)
组件 详细信息

| 抛出 | NullPointerException(如果以下列出的任何参数为 null):

  • baseName

  • locale

  • format

  • loader

  • bundle

|

参数 baseName: 一个完全限定的类名locale: 期望的区域设置format: 资源包格式loader: 应用于加载包的 ClassLoader``bundle: 过期的包loadTime: 包被添加到缓存的时间
返回 true/false 以指示是否需要重新加载过期的包
  • newBundle():
      public ResourceBundle newBundle(String baseName, Locale locale,
       String format, ClassLoader loader, boolean reload)
组件 详细信息

| 抛出 | ClassCastException(如果加载的类无法转换为 ResourceBundleExceptionInInitializerError(如果初始化失败)IllegalAccessException(如果类或构造函数不可访问)IllegalArgumentException(如果格式未知)InstantiationException(如果类实例化失败)IOException(资源读取错误)NullPointerException(如果以下列出的任何参数为 null)😐

  • baseName

  • locale

  • format

  • loader

SecurityException(如果拒绝访问新实例)|

参数 baseName:完全限定的类名locale:期望的区域设置format:资源包格式loader:用于加载包的 ClassLoader``reload:表示资源包是否过期的 true/false 标志
返回 资源包的实例
  • toBundleName():
      public String toBundleName(String baseName, Locale locale)
组件 详细信息
抛出 NullPointerException(如果 baseNamelocale 为 null)
参数 baseName:完全限定的类名locale:期望的区域设置
返回 包名
  • toResourceName():
      public final String toResourceName(String bundleName,
       String suffix)
组件 详细信息
抛出 NullPointerException(如果 bundleNamesuffix 为 null)
参数 bundleName:包名suffix:文件名的后缀
返回 转换后的资源名称

字段和构造函数

ResourceBundle 类有一个字段,如下所述:

    protected Resourcebundle parent

当找不到指定的资源时,getObject 方法会通过父包进行搜索。

ResourceBundle 类的构造函数如下所示:

    public ResourceBundle()
    {
    }

方法

ResourceBundle 类有 18 个方法,每个方法都如下所述:

  • clearCache():
      public static final void clearCache()
组件 详细信息
抛出
参数
返回
      public static final void clearCache(ClassLoader loader)
组件 详细信息
抛出 NullPointerException(如果加载器为 null)
参数 loader:类加载器
返回
  • containsKey():
      public boolean containsKey(String key)
组件 详细信息
抛出 NullPointerException(如果 key 为 null)
参数 key:资源键
返回 根据 ResourceBundle 或父包中是否存在键返回 true/false
  • getBundle():
      public static final ResourceBundle getBundle(String baseName)
组件 详细信息
抛出 MissingResourceException(如果未找到提供的 baseName 的资源包)NullPointerException(如果 baseName 为 null)
参数 baseName:完全限定的类名
返回 基于 baseName 和默认区域设置的资源包
      public static final ResourceBundle getBundle(String baseName,
       Resourcebundle.Control control)
组件 详细信息
抛出 IllegalArgumentException(如果传递的控制操作不当)MissingResourceException(如果未找到提供的 baseName 的资源包)NullPointerException(如果 baseName 为 null)
参数 baseName:完全限定的类名control:控制提供信息以便加载资源包
返回值 基于给定的 baseName 和默认区域设置的资源包
      public static final ResourceBundle getBundle(String baseName,
       Locale locale)
组件 详细信息
抛出 MissingResourceException(如果找不到提供的 baseName 的资源包)NullPointerException(如果 baseNamelocale 为 null)
参数 baseName: 完整的类名locale: 所需的区域设置
返回值 基于给定的 baseNamelocale 的资源包
      public static final ResourceBundle getBundle(String baseName,
       Locale targetLocale, Resourcebundle.Control control)
组件 详细信息
抛出 IllegalArgumentException(如果传递的控制执行不当)MissingResourceException(如果在任何区域设置中找不到提供的 baseName 的资源包)NullPointerException(如果 baseNamecontrollocale 为 null)
参数 baseName: 完整的类名control: 控制提供信息以便资源包可以加载targetLocale: 所需的区域设置
返回值 基于给定的 baseNamelocale 的资源包
      public static final ResourceBundle getBundle(String baseName,
       Locale locale, ClassLoader loader)
组件 详细信息
抛出 MissingResourceException(如果在任何区域设置中找不到提供的 baseName 的资源包)NullPointerException(如果 baseNameloaderlocale 为 null)
参数 baseName: 完整的类名locale: 所需的区域设置loader: 类加载器
返回值 基于给定的 baseNamelocale 的资源包
      public static final ResourceBundle getBundle(String baseName,
       Locale targetLocale, ClassLoader loader,
       ResourceBundle.Control control)
组件 详细信息
抛出 IllegalArgumentException(如果传递的控制执行不当)MissingResourceException(如果在任何区域设置中找不到提供的 baseName 的资源包)NullPointerException(如果 baseNamecontrolloadertargetLocale 为 null)
参数 baseName: 完整的类名control: 提供信息以便资源包可以加载的控制loader: 类加载器targetLocale: 所需的区域设置
返回值 基于给定的 baseNamelocale 的资源包
  • getKeys():
      public abstract Enumeration<String> getKeys()
组件 详细信息
抛出
参数
返回值 ResourceBundle 和父资源包中的键的枚举
  • getLocale():
      public Locale getLocale()
组件 详细信息
抛出
参数
返回值 当前资源包的 locale
  • getObject():
      public final Object getObject(String key)
组件 详细信息
抛出 MissingResourceException(如果找不到提供的键的资源)NullPointerException(如果 key 为 null)
参数 key: 这是所需对象的键
返回值 提供的键的对象
  • getString():
      public final String getString(String key)
组件 详细信息
抛出 ClassCastException(如果找到的对象不是键)MissingResourceException(如果找不到提供的键的资源)NullPointerException(如果 key 为 null)
参数 key: 这是所需 String 的键
返回值 提供的键的 String
  • getStringArray():
      public final String[] getStringArray(String key)
组件 详细信息
抛出 ClassCastException(如果找到的对象不是 String 数组)MissingResourceException(如果找不到提供的键的资源)NullPointerException(如果 key 为 null)
参数 key:这是所需 String 数组的键
返回 提供的键的 String 数组
  • handleGetObject():
      protected abstract Object handleGetObject(String key)
组件 详细信息
抛出 NullPointerException(如果 key 为 null)
参数 key:所需 Object 的键
返回 给定键的对象
  • handleKeySet():
      protected Set<String> handleKeySet()    
组件 详细信息
抛出
参数
返回 ResourceBundle 中的键集合
  • keySet():
      public Set<String> keySet()
组件 详细信息
抛出
参数
返回 ResourceBundle 及其父包中的键集合
  • setParent():
      protected void setParent(ResourceBundle parent)
组件 详细信息
抛出
参数 parent:当前包的父包
返回

Java 9 的变化

基于 ISO-8859-1 的属性文件格式之前由 Java 平台支持。该格式不易支持转义字符,尽管它确实提供了一个适当的转义机制。使用 ISO-8859-1 需要在文本字符及其转义形式之间进行转换。

Java 9 平台包括一个修改后的 ResourceBundle 类,默认文件编码设置为 UTF-8 而不是 ISO-8859-1。这节省了应用程序进行上述转义机制转换所需的时间。

Unicode 7.0.0

Java 增强提案 227,标题为 Unicode 7.0,是为了表明需要更新适当的 API 以支持 Unicode 7.0 版本。该版本的 Unicode 于 2014 年 6 月 16 日发布。在 Java 9 之前,Unicode 6.2 版本是支持的最新版本。

你可以在官方规范页面上了解更多关于Unicode 7.0.0 版本的信息:unicode.org/versions/Unicode7.0.0/.

在本书出版时,最新的 Unicode 标准是 10.0.0 版本,于 2017 年 6 月 20 日发布。有趣的是,Java 9 平台将支持 Unicode 7.0.0 版本,但不支持 Unicode 标准的较新版本 10.0.0。除了这里列出的两个 Unicode 规范之外,从 7.0.0 版本开始,以下内容将不会由 Java 9 平台实现:

  • Unicode 技术标准 #10UTS #10

    • Unicode 排序算法:如何比较 Unicode 字符串的详细信息
  • Unicode 技术标准 #46UTS #46

    • Unicode 国际化域名应用IDNA兼容处理:对文本大小写和域名变体的全面映射

Java 9 平台变化的核心,针对 Unicode 7.0.0 版本的支持,包括以下 Java 类:

  • java.lang package

    • Character

    • String

  • java.text.package

    • Bidi

    • BreakIterator

    • Normalizer

让我们快速查看这些类中的每一个,以帮助我们巩固对 Unicode 7.0.0 支持对 Java 9 平台广泛影响的了解。

java.lang

java.lang.package 提供了在几乎所有 Java 应用程序中使用的根本类。在本节中,我们将探讨 CharacterString 类。

Character 类:

    public final class Character extends Object implements
     Serializable, Comparable<Character>

这是自 Java 第一个版本以来一直存在的许多核心类之一。Character 类的对象由一个类型为 char 的单个字段组成。

String 类:

    public final class String extends Object implements
     Serializable, Comparable<String>, CharSequence

字符串,另一个起源于核心的类,是不可变的字符字符串。

CharacterString 类修改为支持 Unicode 的新版本,即 Java 9 的 7.0,是帮助保持 Java 作为顶级编程语言的重要一步。

java.text

BidiBreakIteratorNormalizer 类不像 CharacterString 类那样广泛使用。以下是这些类的简要概述。

Bidi 类:

    public final class Bidi extends Object

此类用于实现 Unicode 的双向算法。这用于支持阿拉伯语或希伯来语。

关于 Unicode 双向算法 的具体信息,请访问 unicode.org/reports/tr9/.

BreakIterator 类:

    public abstract class BreakIterator extends Object
     implements Cloneable

此类用于查找文本边界。

Normalizer 类:

    public final class Normalizer extends Object

此方法包含两个方法:

  • isNormalized:用于确定给定序列的 char 值是否已规范化

  • normalize:将一系列 char 值规范化

附加重要性

如前所述,JDK 8 支持 Unicode 6.2。版本 6.3 于 2013 年 9 月 30 日发布,以下列出了一些亮点:

  • 双向行为改进

  • 改进了 Unihan 数据

  • 更好的希伯来语支持

版本 7.0.0,于 2014 年 6 月 16 日发布,引入了以下更改:

  • 增加了 2,834 个字符

    • 对阿塞拜疆语、俄语和德语方言的支持增加

    • 图形符号

    • 几个国家和地区的史前脚本

  • Unicode 双向算法更新

  • 近 3,000 个新的粤语发音条目

  • 对印度语脚本属性的重大改进

Unicode 版本 6.3 和 7.0.0 的巨大变化强调了 Java 9 平台支持 7.0.0 而不是 6.2(如 Java 8)的重要性。

Linux/AArch64 端口

Java 增强提案 237JEP 237)的唯一目标是移植 JDK 9 到 Linux/AArch64。为了了解这对我们作为 Java 9 开发者意味着什么,让我们简要谈谈硬件。

ARM 是一家英国公司,已经超过三十年在创造计算核心和架构。他们的原始名称是 Acorn RISC Machine (ARM),其中 RISC 代表 Reduced Instruction Set Computing。在某个阶段,他们将其名称更改为 Advanced RISC Machine (ARM),最终变为 ARM Holdings 或简称为 ARM。他们向其他公司许可其架构。ARM 报告称,已经制造了超过 1000 亿个 ARM 处理器。

在 2011 年晚些时候,ARM 推出了一种名为 ARMv8 的新 ARM 架构。这个架构包括一个可选的 64 位架构 AArch64,正如你所期望的,它带来了一个新的指令集。以下是 AArch64 功能的简略列表:

  • A64 指令集:

    • 31 个通用 64 位寄存器

    • 专用零或栈指针寄存器

    • 能够接受 32 位或 64 位参数

  • 高级 SIMD (NEON) - 增强:

    • 32 个 128 位寄存器

    • 支持双精度浮点数

    • AES 加密/解密和 SHA-1/SHA-2 哈希

  • 新的异常系统

Oracle 在识别这种架构为需要在新的 Java 9 平台上支持方面做得很好。据说新的 AArch64 架构基本上是一个全新的设计。JDK 9 已成功移植到 Linux/AArch64,以下是一些实现:

  • 模板解释器

  • C1 JIT 编译器

  • C2 JIT 编译器

关于 C1 和 C2 JIT 编译器的信息,请参阅第十四章,命令行标志

多分辨率图像

Java 增强提案 251 的目的是创建一个新的 API,该 API 支持多分辨率图像。具体来说,允许一个多分辨率图像封装同一图像的多个分辨率变体。这个新的 API 将位于 java.awt.image 包中。以下图表显示了多分辨率如何将不同分辨率的图像集封装到一个单独的图像中:

图片

这个新的 API 将使开发者能够检索所有图像变体或检索特定分辨率的图像。这是一组强大的功能。java.awt.Graphics 类将用于从多分辨率图像中检索所需的变体。

这里是 API 的快速浏览:

    package java.awt.image;

    public interface MultiResolutionImage  
    {
      Image getResolutionVariant(float destinationImageWidth,
       float destinationImageHeight);

      public List <Image> getResolutionVariants();
    }

如前述代码示例所示,API 包含 getResolutionVariantgetResolutionVariants,分别返回一个 Image 和图像列表。由于 MultiResolutionImage 是一个接口,我们需要一个抽象类来实现它。

常见区域数据存储库 (CLDR)

Java 增强提案 252 默认使用 CLDR 区域数据,默认实现从 Unicode 通用区域数据存储库使用区域数据的决策。CLDR 是许多支持多种语言的软件应用程序的关键组件。它被誉为最大的区域数据存储库,并被苹果、谷歌、IBM 和微软等大量大型软件提供商使用。CLDR 的广泛应用使其成为区域数据的非官方行业标准存储库。在 Java 9 平台中将它作为默认存储库进一步巩固了其在软件行业标准中的地位。

有趣的是,CLDR 已经是 JDK 8 的一部分,但不是默认库。在 Java 8 中,我们必须通过设置系统属性来启用 CLDR,如下所示:

    java.locale.providers=JRE,CLDR

因此,在 Java 9 中,我们不再需要启用 CLDR,因为它将是默认存储库。

Java 9 平台中还有其他区域数据存储库。它们按默认查找顺序列在这里:

  1. 通用区域数据存储库CLDR)。

  2. COMPAT - 之前是 JRE。

  3. 服务提供者接口SPI)。

要更改查找顺序,我们可以更改java.locale.providers设置,如下所示:

    java.locale.providers=SPI,COMPAT,CLDR

在前面的示例中,SPI将首先出现,然后是COMPAT,最后是CLDR

摘要

在本章中,我们关注了 Java 9 平台提供的附加实用工具的最佳实践。具体来说,我们涵盖了 UTF-8 属性文件、Unicode 7.0.0、Linux/AArch64 端口、多分辨率图像和通用区域数据存储库。

在下一章,我们最后一章,我们将通过展望 Java 10 可以期待的内容来探讨 Java 平台的未来方向。

第十六章:未来方向

在上一章中,我们关注了 Java 9 平台提供的某些激动人心的实用工具的最佳实践。具体来说,我们涵盖了 UTF-8 属性文件、Unicode 7.0.0、Linux/AArch64 端口、多分辨率图像和常见区域数据存储库。

本章概述了 Java 平台在 Java 9 之后的未来开发。我们将探讨 Java 10 的计划以及未来可能看到的进一步变化。每个对 Java 平台的潜在更改将被描述为目标、提交或草案。目标指的是已标记为 Java 10 的更改。提交指的是已提交但未针对 Java 平台特定版本的更改。草案更改仍在规划中,尚未准备好提交或指定为目标。

具体来说,本章涵盖了以下类别中组织的 Java 平台未来的更改:

  • JDK 更改

  • Java 编译器

  • Java 虚拟机

  • JavaX

  • 特殊项目

JDK 未来的更改

Java 开发工具包(Java Development Kit,简称 JDK)是 Java 平台的核心,并且随着每个版本的发布,持续更新以实现新的功能和效率。展望 Java 9 之后,我们看到了 JDK 可能发生的众多变化。其中许多变化将在 Java 10 中实现,其他一些可能被保留到以后的版本中。

Java 10 及以后的 JDK 更改在以下提案类别中呈现:

  • 针对 Java 10

  • 提交的提案

  • 草案提案

针对 Java 10 的 JDK 更改

在本书出版时,以下列出的Java 开发工具包JDK)相关更改计划包含在 Java 10 平台中:

  • 存储库整合

  • 原生头工具移除

存储库整合

Java 9 平台由以下图中所示的八个不同的存储库组成。在 Java 10 中,我们应该看到所有这些存储库合并为一个单一存储库:

图片

存储库整合应有助于简化开发。此外,它应增加维护和更新 Java 平台的便利性。

您可以提前查看此存储库:hg.openjdk.java.net/jdk10/consol-proto/

原生头工具移除

javah 工具用于从 Java 类生成 C 头文件和 C 源文件。C 程序可以引用生成的头文件和源文件。

下面是 javah 工具的诞生与消亡:

图片

如前所述,javah 工具是在 Java 7 中引入的,其功能包含在随 JDK8 提供的 javac 中。据报道,与原始工具相比,该功能更优越。在 JDK 9 中,每次使用 javah 工具时,开发者都会收到警告,告知他们该工具将从 JDK 中移除。该工具计划在 JDK 10 中移除。

与 JDK 相关的提交提案

以下 Java 增强提案已经提交,但尚未承诺作为 Java 10 平台的一部分进行交付。Oracle 设定了两年发布计划,因此可以合理假设,本节及以后列出的许多(如果不是全部)提案都有可能成为 Java 10 平台的一部分:

  • 在 CMS 中并行化完全 GC 阶段

  • JMX 的 REST API

  • 支持堆分配

在 CMS 中并行化完全 GC 阶段

在第七章《利用新的默认 G1 垃圾收集器》中,我们回顾了并发标记清除CMS)垃圾收集器的变化。CMS 垃圾收集涉及扫描堆内存,标记要删除的对象,然后进行清除以实际删除这些对象。CMS 垃圾收集方法本质上是一种升级的“标记和清除”方法;您可以参考第七章《利用新的默认 G1 垃圾收集器》,以获取更多信息。

当前 CMS 垃圾收集的缺点是,串行标记和清除使用单个线程实现。这导致不希望的暂停时间。目前,完全垃圾收集在四个阶段进行:

  • 标记阶段:标记要收集的对象

  • 转发阶段:确定活动对象将被重新定位的位置

  • 调整指针阶段:根据活动对象的新位置更新指针

  • 压缩阶段:将对象移动到指定的位置

CMS 的未来计划是实现标记和清除,以便它们可以并行执行。这种变化不是对垃圾收集算法的改变。相反,上述列出的每个阶段都将并行化。这将使 CMS 垃圾收集更加高效,并有望消除或显著减少暂停时间。

JMX 的 REST API

表示状态转移REST)、RESTful 编程和 RESTful API 使用客户端/服务器缓存通信协议,通常是 HTTP。REST 是开发网络应用程序的常见软件架构。

Java 平台未来的一个变化是提供 RESTful 网络接口给 MBeans。

托管 BeanMBean)是 Java 中代表要管理的资源的对象。这些资源可能包括特定的硬件设备、应用程序、服务或其他组件。

接口将允许 MBeans 使用以下 HTTP 方法:

  • CONNECT

  • DELETE

  • GET

  • HEAD

  • OPTIONS

  • POST

  • PUT

  • TRACE

MBeans 使用 Java 管理扩展JMX)进行管理。JMX 架构有三个级别,如下图所示:

如您所见,REST 适配器是 分布式服务 级别的一部分。该级别包含连接器和适配器。连接器提供代理级别接口到远程客户端的镜像。另一方面,适配器使用不同的协议转换接口。未来的变化将是将 代理 级别的服务转换为 REST API。

支持堆分配

提出的未来变化是允许开发者为 Java 堆指定替代内存设备。具体来说,建议允许开发者指定非 DRAM 内存用于 Java 堆。这一变化利用了内存和内存设备的成本下降。

实现可能使用 AllocateHeapAt 标志。

JDK 相关的草案提案

本节涵盖了几个与 JDK 相关的提案,在本书出版时,它们处于草案阶段。这表明它们可能没有得到充分分析,甚至可能被取消。尽管如此,这些提案中的每一个很可能从草案阶段发展到提交阶段,然后针对 Java 10 平台进行目标定位。

本节涵盖的草案提案如下:

  • 最终化及时性

  • Java 内存模型

  • 外部函数接口

  • 隔离方法

  • 减少元空间浪费

  • 改善 IPv6 支持

  • 方法句柄的无包装参数列表

  • 使用值类型增强的 MandelblotSet 示例

  • 高效的数组比较内联函数

最终化及时性

Java 语言包括最终化来清理垃圾收集无法到达的对象。提出的更改是使此过程更快,并将需要修改以下内容:

  • ReferenceHandleThread

  • FinalizerThread

  • java.lang.ref.Reference

与提高最终化及时性相关的额外变化包括创建一个新的 API。以下图形详细说明了该 API 将如何实现 GC 和运行时操作,然后通知需要进行最终化。这肯定会导致处理速度更快:

Java 内存模型

持续努力以保持 Java 的内存模型JMM)更新。当前的工作重点集中在以下几个领域,包括:

  • 共享内存并发

  • JVM 并发支持

  • JDK 组件

  • 工具

JMM 相关工程努力的预期结果如下:

  • 改进的形式化

  • JVM 覆盖率

  • 扩展范围

  • C11/C++11 兼容性

  • 实施指南

  • 测试支持

  • 工具支持

外部函数接口

外部函数接口FFI)是软件 API,允许程序调用用不同语言编写的程序中的方法/函数。在 JDK 的一个即将到来的版本中,我们可能会看到一种 FFI,允许开发人员直接从 Java 方法调用共享库和操作系统内核。据说提议的 FFI 还将使开发人员能够管理本地内存块。

新的 FFI 将类似于 Java Native AccessJNA)和 Java Native RuntimeJNR)。JNA 是一个库,允许在不使用 Java Native InterfaceJNI)的情况下访问本地共享库。JNR 是一个用于调用本地代码的 Java API。提议的 FFI 将允许并优化本地方法调用以及优化的本地内存管理。

独立的方法

MethodHandles.Lookup 类是 java.lang.invoke 包的一部分。我们使用查找对象来创建方法句柄,并使用查找类来访问它们。以下是查找类的头文件:

    public static final class MethodHandles.Lookup extends Object

MethodHandles.Lookup 类的未来更改将支持在不附加类的情况下加载方法字节码。此外,这些方法将使用方法句柄进行引用。该类将有一个新的 loadCode 方法。

减少元空间浪费

目前,当元空间块被释放时,它们不能作为不同大小的块使用。因此,如果元空间块 A 被释放且大小为 X,那么该空间不能用于大于或小于大小 X 的元空间块。这导致大量不可用的元空间浪费。这也可能导致内存不足错误。

JDK 的未来更改将通过增加元空间块的重用来解决这个问题。该更改将支持以下情况:

  • 允许相邻块形成一个更大的块

  • 允许较大的块被分割成较小的块

本提议的更改通过确保较小的块可以被重用,以及较大的块不会被浪费(因为它们可以被分割以支持较小块的重用)来解决这个问题。

改进 IPv6 支持

互联网协议版本 6IPv6)是当前互联网协议的版本。互联网协议提供了识别和位置模式,使得互联网流量路由成为可能。IPv6 被视为一个用于分组交换网络的互联网层协议。

以下图显示了互联网协议的历史:

图片

IPv6 是 IPv4 的替代品,并且有几个变化,Java 平台应该支持。从 IPv4 到 IPv6 的关键变化如下分类:

  • 大数据包

  • 更大的地址空间

  • 移动性

  • 多播

  • 网络层安全

  • 选项可扩展性

  • 隐私

  • 简化路由处理

  • 无状态地址自动配置

随着互联网从 IPv4 向 IPv6 的过渡,以下情况是可能的,并且都应该在 Java 10 平台上得到支持:

  • 存在多个 IPv4 版本

  • 存在一种 IPv6 版本

  • 存在多种 IPv6 版本

  • 存在多种 IPv4 版本和一种 IPv6 版本

  • 存在多种 IPv4 和 IPv6 版本

方法句柄的无包装参数列表

目前无包装参数列表的处理方式可能导致处理效率低下。这在我们使用Object[]List<object>作为可变长度参数列表时尤其如此。Java 使用java.lang.invoke来转换使用装箱的方法调用。在 Java 中,自动装箱是编译器自动将原始类型及其相应的对象包装类进行转换。以下是包装类及其对应原始类型的列表:

包装类 原始类型
布尔值 boolean
字节 byte
字符 char
双精度浮点数 double
浮点数 float
整数 int
长整型 long
短整型 short

如以下插图所示,当我们从原始值转换为相关包装类的对象时发生自动装箱,当我们从包装类的对象转换为原始值时称为拆箱:

图片

不效率是由于参数列表的实际类型与封装它们的数组或列表之间的不匹配。在未来的 Java 版本中,这些不效率将被消除。Java 平台将添加一个新的ArgumentList类,该类将多态地将有效的参数列表装箱到堆节点中。

使用值类型的增强 MandelblotSet 演示

这个低优先级的 Java 增强提案很可能在 Java 10 中实现,因为其范围有限。计划是开发一个示例 Java 应用程序,展示使用 Valhalla 项目组件、值类型和泛型而不是原始类型在内存和性能方面的改进。

Valhalla 项目组件指的是用户定义的自定义不可变原始类型为值类型。

你可以在本章的 Java 虚拟机部分了解更多关于值类型的信息。

曼德布罗特集是混沌理论中使用的分形数学的一个具体例子。随 JDK 8 附带的 MandelbrotSet 示例提供了并行和顺序数据流的比较。在 Java 10 或更高版本中,MandelbrotSet 示例将被更新,以展示使用 Valhalla 项目组件、值类型和泛型与使用原始类型之间的性能和内存效率。

高效的数组比较内建函数

未来对 Java 平台的一个变化是包含一个比较数组的函数。目前,这是开发者必须自己编写的。这个变化将通过添加类似于java.util.Arrays中的compareTo方法来实现。

虽然具体细节尚不可知,但能够使用原生功能比较数组的前景令人兴奋。这将是一个能节省许多开发者时间的组件。这很可能会在 Java 10 平台版本中实现。

Java 编译器的未来变化

有两个值得注意的 Java 平台草案变更,特别是 Java 编译器。以下列出了这些 Java 增强提案,并在本节中详细说明:

  • 退役 javac -source-target选项的政策

  • 可插拔静态分析器

退役 javac -source 和-target 选项的政策

已提交一个正式草案提案,以定义退役-source-target选项的政策。这项工作旨在帮助降低编译器的维护成本。-source-target选项是为了简化开发工作而提供的,但并非任何标准所正式要求。从 Java 9 平台开始,这些目标选项不再被识别。

新政策被称为“一加三回”,这意味着当前版本将得到支持,以及之前的三个版本。这个政策将延续到 JDK 10。

可插拔静态分析器

2013 年夏季启动了一个持续进行的 Java 增强提案研究,作为探索性措施和未来对完整 Java 增强提案的支持,以使开发者能够定义在编译时可以执行任意静态分析的扩展。这项研究旨在了解如何为 Java 编译器实现一个可插拔的静态类型分析器框架。

研究的目标如下:

  • 收集静态分析器需求

  • 分析静态分析器

  • 确定支持静态分析器的框架的要求

  • 实施和测试

持续研究最终的结果将是提交一个功能 Java 增强提案,或者建议停止追求该功能。

Java 虚拟机的未来变化

已提交并起草了几个针对 Java 虚拟机(JVM)和核心库的新功能和增强。这些功能和增强至少有一部分可能会在 Java 10 平台上实现,其他则可能留待后续版本。

与 JVM 相关的提交提案

已提交三个 Java 增强提案。虽然目前没有指定为 Java 10,但很可能在 Java 10 发布时我们会看到这些变化。以下列出了这三个提案:

  • 容器感知 Java

  • 在 GPU 上启用 Java 方法的执行

  • Epsilon GC:任意低开销的垃圾(非)收集器

容器感知 Java

正在努力使 JVM 和核心库在运行在容器中时能够感知到。此外,为了能够适应使用可用的系统资源。这个特性与云计算的普遍性特别相关。

提案的功能有两个主要组成部分:

  • 检测:

    • 确定 Java 是否在容器内运行
  • 容器资源暴露:

    • 暴露容器资源限制

    • 暴露容器资源配置

已初步确定了几个配置状态点:

通用 CPU 相关 内存相关
isContainerized CPU 时期 块 I/O 设备权重
CPU 配额 块 I/O 权重
CPU 集内存节点 当前内存使用率
CPU 集合 设备 I/O 读取速率
CPU 使用率 设备 I/O 写入速率
每个 CPU 的 CPU 使用率 最大内存使用率
CPU 数量 最大内核内存
内存交换率
OOM 杀死启用
OOM 分数调整
共享内存大小
软内存限制
总内存限制

初始时,此功能计划支持 Linux-64 上的 Docker。一个可能的场景是,此功能将与 Java 10 一起发布,仅支持 Linux-64 上的 Docker。然后,在 Java 平台后续版本中,将扩展功能支持。

在 GPU 上默认启用 Java 方法的执行

启用 Java 应用程序无缝利用 GPU 的能力是 Sumatra 项目的主题。目标是使用 Java 的 Stream API 并行和 lambda 编程模型。利用 GPU 的处理能力和效率对我们来说非常有意义。

总体目标是使此功能对开发者来说易于使用。此功能将具有以下特性:

  • 不要更改 Java 并行流 API 的语法

  • 硬件和软件堆栈应自动检测

  • 自动检测和分析,以确定从性能标准来看使用 GPU 是否合理

  • 在将处理卸载到 GPU 失败时提供 CPU 执行

  • 将不会出现性能下降

  • 此功能不会引入新的安全风险

  • CPU 和 GPU 之间将会有内存持久性

此 Java 增强提案的关键好处将是提高我们的 Java 应用程序的性能。

Epsilon GC - 随意低开销的垃圾(非)收集器

在第七章中,利用新的默认 G1 垃圾回收器,我们详细介绍了 Java 平台发布 Java 9 时对垃圾回收的改进。为了持续改进,已经提交了一个 Java 增强提案,以开发一个专门处理内存分配的垃圾回收器。当 Java 堆上没有更多内存可用时,这个垃圾回收器将向 JVM 发送关闭信号。

目标是让这个垃圾回收器保持被动,并使用非常有限的额外开销。引入这种垃圾回收的目的是不降低性能。

此更改不会影响当前的垃圾回收器。

JVM 相关的提案

以下 Java 增强提案已为 Java 平台的未来版本制定,并在本节中详细说明:

  • 在 JVM 编译方法上提供稳定的 USDT 探测点

  • 并发监视器膨胀

  • 以低开销方式采样 Java 堆分配

  • 诊断命令框架

  • 增强类重定义

  • 在适当的情况下默认启用 NUMA 模式

  • 值对象

  • 对齐 JVM 访问检查

在 JVM 编译方法上提供稳定的 USDT 探针点

用户级统计定义跟踪USDT)用于插入探针点以标记方法的进入和退出。编译器然后允许与跟踪工具进行握手,以便这些工具可以发现探针点并对其进行操作。

常见的跟踪工具有 Dtrace 和伯克利数据包过滤器BPF)。

即使是 JVM 9,Java 虚拟机也不支持此技术集。当前缺乏支持源于 JVM 生成编译代码的方式;它以动态方式生成,没有任何静态可执行链接文件ELFs)。跟踪工具需要 ELFs 才能工作。另一个缓解因素是 JVM 动态地修补其生成的代码,这些代码不支持外部修补。

在未来的 Java 版本中,可能是 Java 10,JVMTIJVM 工具接口)将被修改以支持探针工具在 JVM 的动态编译代码上执行其标准操作。暂时确定的 JVMTI API 更改包括:

  • 添加补丁点或方法入口和出口

  • 编译方法的枚举

  • 编译方法加载时的状态变更通知

  • 查询支持

  • 切换跟踪点的开关

  • 使编译方法的块可检查

好消息是,不需要对 Java 代码的编译方式进行任何更改。它已经可以打补丁,因此所需的功能将通过修改 USDT API 以及 JVM 的一些更改来创建。

并发监控降级

在我们的上下文中,监控器是一个同步机制,用于控制对对象的并发访问。监控器有助于防止多个线程同时访问被监控的对象。JVM 自动在三种监控器实现方法之间切换。以下是对三种实现方法的说明:

图片

Java 对象的初始锁定使用偏向锁定。该方法确保只有锁定线程可以锁定对象。采用这种方法,JVM 在 Java 对象中安装一个线程指针。当第二个线程尝试锁定 Java 对象时,JVM 切换到基本的锁定监控器实现方法。第二种方法使用比较和交换CAS)操作。当 CAS 操作失败时,例如当第二个线程尝试锁定 Java 对象时,JVM 切换到第三种监控器实现方法。该方法是一个完整的监控器。该方法需要本地堆存储,被称为监控器膨胀。

并发监控降级 Java 增强提案的目的是在线程运行时执行监控降级。这将减少由 JVM 引起的暂停时间。

提供一种低开销的方式来采样 Java 堆分配

管理不当的 Java 堆可能导致堆耗尽,以及由于内存碎片化(GC 抖动)导致的内存不足。在 Java 的未来版本中,很可能是 Java 10,我们将有一种方法来采样 Java 堆分配。这将通过增强Java 虚拟机工具接口JVMTI)来实现。结果的功能将提供一个极低开销的解决方案。

诊断命令框架

Java 增强提案 137,诊断命令框架,提议创建一个框架,用于向 Java 虚拟机发送诊断命令。

该框架将包括一个Java 管理扩展(JMX)接口,该接口将允许通过 JMX 连接远程发送诊断命令。

JRocket 任务控制工具已经成功实现了这一功能。这证明了概念,因此这一增强功能很可能将成为 Java 10 平台的一部分。

增强类重定义

Java 增强提案 159,增强类重定义,要求在运行时增强 JVM 的类重定义能力。具体来说,该提案包括以下类重定义操作:

  • 添加超类型

  • 添加方法

  • 添加静态字段

  • 添加实例字段

  • 移除方法

  • 移除静态字段

  • 移除实例字段

当前 JVM 的类重定义能力仅限于方法交换。这被视为非常限制性的。在新提议的增强功能中,开发者不需要在更改后重新启动他们的应用程序。这对于处理大型和分布式系统特别有益。

在适当的情况下默认启用 NUMA 模式

Java 增强提案 163,在适当的情况下默认启用 NUMA 模式。此提案仅适用于 NUMA 硬件。目的是当 JVM 检测到 NUMA 硬件时,启用以下标志:

    XX:+UseNUMA

当前此标志可以通过手动调用。在提议的增强功能中,当 JVM 检测到它正在 NUMA 硬件上运行时,它将自动调用。

非一致性内存访问NUMA)是计算机多处理中使用的内存模型。在这个内存模型中,访问时间取决于内存位置相对于处理器的位置。

这将是一个易于实现的增强功能,并可能成为 Java 10 平台发布的一部分。

值对象

Java 增强提案 169,值对象,旨在提供必要的 JVM 基础设施,以允许处理不可变对象以及无引用对象。这个新的基础设施将允许使用非原始数据类型进行高效的按值计算。

本提案的目标包括以下内容:

  • 更紧密地对齐java.lang.Integerint的语义。

  • 使 Java 数据结构更便携

  • 支持与 Java 原始数据类型性能相似的性能的抽象数据类型:

    • 用户定义的

    • 库定义的

  • 通过启用纯数据函数式计算来优化并行计算

  • 改进对以下内容的支持:

    • 复数

    • 向量值

    • 元组

  • 提高安全性和安全性

  • 减少“防御性复制”

声明的实现策略之一是添加一个lockPermanently操作。它将传递一个对象,然后将其标记为不可变且不可别名。永久锁定对象的观念规定:

  • 字段不能更改

  • 数组元素不能更改

  • 无法进行同步

  • 无法调用'等待'方法

  • 无法调用'通知'方法

  • 不允许进行身份哈希码查询

  • 无法执行指针相等性检查

这可能是 Java 10 平台最受欢迎的添加之一。

对齐 JVM 访问检查

Java 增强提案 181,将嵌套类的 JVM 检查与 Java 语言规则对齐,重点关注将 JVM 访问检查规则与 Java 语言规则对齐的需求,特别是对于嵌套类中的构造函数、字段和方法。这将通过将相关类在嵌套中进行分区来实现。类文件将能够访问同一嵌套中其他类文件的私有名称。

嵌套将共享一个访问控制上下文。随着嵌套的出现,访问桥接将不再需要。大部分的变化将是对 JVM 的访问规则。

未来对 JavaX 的更改

Javax.*包是两个特定 Java 增强提案的主题,这些提案已提交给未来的 Java 平台发布。以下为这些提案:

  • JMX 特定注解用于注册管理资源

  • 现代化 GTK3 外观和感觉实现

JMX 特定注解用于注册管理资源

标题为“JMX 特定注解用于注册管理资源”的草案 Java 增强提案将提供一组用于注册和配置MBeans管理 Bean)的注解。

MBean 是一个表示可管理资源的 Java 对象(应用程序、服务、组件或设备)。

本提案的目标是减轻开发者在注册和配置 MBeans 时的负担。此外,通过确保所有 MBean 声明组件都位于同一位置,将提高源代码的可读性。

JMX 特定注解将位于javax.management.annotations包中。

这个 Java 增强提案是专门为 Java 11 规划的。尽管如此,它有可能被重新设计为 Java 10。

现代化 GTK3 外观和感觉实现

GTK3 是一个用于创建图形用户界面的小部件工具包,正式名称为 GIMP 工具包。名为“现代化 GTK3 外观和感觉实现”的 Java 增强提案草案呼吁重写当前的 GTK2 外观和感觉,以便使用 GTK3。

GTK3 实现将不会取代 GTK2。重要的是要注意,在运行时只能使用其中一个,而不能同时使用这两个。

您可以在developer.gnome.org/gtk3/stable/访问 GTK3 参考手册。

进行中的特殊项目

Java 增强提案(JEP)提出了对 Java 平台的设计和实现更改。一个 JEP 被起草的标准是工作必须至少满足以下条件之一:

  • 至少两周的工程工作

  • 表示对 JDK 的重大更改

  • 代表了开发人员或客户的高需求问题

项目,另一方面,代表了由以下某个团体资助的协作努力:

  • 2D 图形

  • 采用

  • AWT

  • 构建

  • 兼容性和规范审查

  • 编译器

  • 符合性

  • 核心库

  • 管理委员会

  • HotSpot

  • 国际化

  • JMX

  • 成员

  • 网络

  • NetBeans 项目

  • 端口

  • 质量保证

  • 安全

  • 可服务性

  • 声音

  • Swing

  • 网络

小组是正式的,新的小组可以被提议。

以下列出的活跃项目代表了 Java 平台可能的未来增强领域。本节后面将提供每个项目的简要信息,并提供了对未来变化的一般领域的见解:

  • 注解管道 2.0

  • 音频合成引擎

  • Caciocavallo

  • Common VM 接口

  • 编译器语法

  • 达芬奇机器

  • 设备 I/O

  • Graal

  • HarfBuzz 集成

  • 科纳

  • OpenJFX

  • 巴拿马

  • 沙南多亚

注解管道 2.0

该项目探讨了如何改进 Java 编译器管道中注解的处理方式。没有意图提出更改规范;而是重点在于性能提升。

音频合成引擎

该项目正在研究为 JDK 创建一个新的 midi 合成器。当前的 midi 合成器属于一个授权库。工作组希望新的 midi 合成器作为一个开源 JDK 资产。

Caciocavallo

Caciocavallo 项目旨在改进 OpenJDK 的抽象窗口工具包AWT)内部接口。这扩展到 2D 子系统。拟议的改进旨在简化 AWT 移植到新平台的方式。

Common VM 接口

Common VM 接口项目旨在记录 OpenJDK 的 VM 接口。这应该使 Classpath VM 和其他 VM 使用 OpenJDK 变得更加容易。

编译器语法

编译器语法项目正在开发一个基于 ANTLR 语法的实验性 Java 编译器。ANTLRAnother Tool for Language Recognition)是一个解析器,它读取、处理和执行结构化文本或二进制文件。项目团队希望这个 Java 编译器能够取代当前的编译器,因为它使用的是手写的解析器,LALRLook-Ahead Left to Right)。项目小组已将 LALR 解析器识别为脆弱且难以扩展。

达芬奇机器

达芬奇机器项目代表了将 JVM 扩展以支持非 Java 语言的努力。当前的工作重点在于允许新语言与 Java 在 JVM 中并存。性能和效率是该努力的关键特性。

设备 I/O

该项目旨在通过 Java 级别的 API 提供对通用外围设备的访问。项目团队希望支持的初始外围设备列表包括:

  • GPIO通用输入/输出

  • I2C集成电路间总线

  • SPI串行外围接口

  • UART通用异步收发传输器

Graal

Graal 项目的目标是通过 Java API 暴露 VM 功能。这种暴露将允许开发者针对特定的语言运行时用 Java 编写动态编译器。这项工作包括开发一个多语言解释器框架。

HarfBuzz 集成

HarfBuzz 集成项目旨在将 HarfBuzz 布局引擎集成到 Java 开发工具包中。这是为了用 HarfBuzz 布局引擎替换 ICU 布局引擎。ICU 布局引擎已被弃用,这巩固了该项目未来成功的重要性。

Kona

Kona 项目正在努力定义和实现支持 物联网IoT) 领域的 Java API。这包括网络技术和协议。尽管没有明确说明,但安全和安全性将是该努力实施成功的关键。

OpenJFX

关于 OpenJFX 项目没有太多细节。该项目声明的目标是创建下一代 Java 客户端工具包。根据项目标题,可以假设该小组希望创建一个 OpenJFX 版本的 JavaFX,JavaFX 是一组用于创建富互联网应用的包。

Panama

Project Panama 专注于增强 JVM 和非 Java API 之间的连接。该项目包括以下选定的组件:

  • 原生函数调用

  • 从 JVM 进行原生数据访问

  • JVM 堆内的原生数据访问

  • JVM 堆中的新数据布局

  • 头文件 API 提取工具

项目团队已生成一个与 JDK 9 结构相匹配的仓库树。这显著增加了项目成功的可能性。

Shenandoah

Project Shenandoah 的目标是显著减少垃圾收集操作的中断时间。方法是让更多的垃圾收集操作与 Java 应用程序并发运行。在 第七章 中,利用新的默认 G1 垃圾收集器,你了解了 CMS 和 G1。Shenandoah 项目打算将并发压缩添加到可能的垃圾收集方法中。

摘要

在本章中,我们概述了 Java 平台未来的发展,超出了 Java 9。我们探讨了 Java 10 的计划以及我们可能看到的 Java 10 以上的进一步变化。每个可能的 Java 平台变化都被描述为有针对性的、已提交的或草案。具体来说,我们涵盖了以下类别中 Java 平台的未来变化:JDK 变更、Java 编译器、Java 虚拟机、JavaX 和特殊项目。

posted @ 2025-09-12 13:56  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报