精通-Java-11-全-
精通 Java 11(全)
零、前言
Java11 及其新特性增加了语言的丰富性,这是构建健壮软件应用最常用的编程语言之一。Java11 扩展了 Java 平台的功能。本书是您掌握自 Java9 以来对 Java 平台所做更改的一站式指南。
本书概述并解释了 Java11 中引入的新特性及其重要性。我们将为您提供实用的指导,帮助您将新学到的 Java11 知识以及有关 Java 平台未来发展的信息应用到 Java 平台上。这本书的目的是提高您的生产率,使您的应用更快。通过学习 Java 中的最佳实践,您将成为组织中 Java 的准用户。
在本书的结尾,您不仅将学习 Java11 的重要概念,而且还将对使用这种伟大语言编程的重要方面有一个细微的了解
这本书是给谁的
本书面向企业开发人员和现有 Java 开发人员。掌握 Java 的基本知识是必要的。
这本书的内容
第一章“Java11 场景”探讨了 Java 平台上新实现的基于时间的版本控制系统。我们调查了当前的 Java 环境,特别关注 Java9、10(18.3)和 11(18.9)带来的变化。我们的探索包括对 Java9 的模块化、Javashell、外部进程控制、垃圾收集、JHM 等的概述。对于 Java10,我们将重点介绍关键更改,包括局部变量类型推断、JDK 整合、垃圾收集、应用类数据共享和根证书等。最后,我们将探讨 Java11 中引入的更改,包括动态类文件常量、垃圾收集和 Lambda 的局部变量类型推断。
第 2 章“探索 Java11”,关注 Java 平台中引入的几个内部变化,包括 Java9、10 和 11 的变化。Java9 代表 Java 平台的主要版本;Java10 和 11 是定时版本。总的来说,这些版本包含了大量的内部更改,为 Java 开发人员提供了一系列新的可能性,有些源于开发人员的请求,有些源于 Oracle 的增强
第 3 章“Java11 基础”,介绍了影响变量处理器的 Java 平台变更、导入语句、Project Coin 改进、局部变量类型推断、根证书、动态类文件常量等。这些表示对 Java 语言本身的更改。
第 4 章“使用 Java11 构建模块化应用”,分析了 Jigsaw 项目指定的 Java 模块的结构,深入探讨了 Jigsaw 项目作为 Java 平台的一部分是如何实现的。我们还回顾了 Java 平台与模块化系统相关的关键内部更改。
第 5 章“将应用迁移到 Java11”,探讨如何将现有的应用迁移到当前的 Java 平台。我们将研究手动和半自动迁移过程。本章旨在为您提供一些见解和过程,使您的非模块化 Java 代码能够在当前的 Java 平台上工作。
第 6 章“Java Shell 实验”,介绍了新的命令行,Java 中的读取求值打印循环(也称为 REPL 工具),以及 Java Shell(JShell)。我们从介绍该工具、REPL 概念开始,然后进入 JShell 使用的命令和命令行选项。我们采用实践者的方法来回顾 JShell,并包含一些您可以自己尝试的示例。
第 7 章“利用默认的 G1 垃圾收集器”,深入研究了垃圾收集及其在 Java 中的处理方式。我们从垃圾收集的概述开始,然后看看 Java9 之前的领域中的细节。有了这些基本信息,我们就来看看 Java9 平台中具体的垃圾收集更改。最后,我们来看一些即使在 Java11 之后仍然存在的垃圾收集问题。
第 8 章“使用 JMH 的微基准应用”,介绍如何使用 Java 微基准线束(JMH)编写性能测试,这是一个用于为 Java 虚拟机(JVM)编写基准测试的 Java 线束库。我们使用 Maven 和 JMH 来帮助说明使用新 Java 平台进行微标记的威力。
第 9 章“利用进程 API”,重点介绍了Process类和java.lang.ProcessHandleAPI 的更新。在 Java 的早期版本中,在 Java9 之前,用 Java 管理进程是很困难的。API 不够,有些功能不够,有些任务需要以特定于系统的方式来解决。例如,在 Java8 中,让进程访问自己的进程标识符(PID)是一项不必要的困难任务。
第 10 章“细粒度栈跟踪”,重点介绍 Java 的StackWalkerAPI。API 支持普通程序很少需要的特殊功能。API 在一些非常特殊的情况下非常有用,比如框架提供的功能。因此,如果您想要一种有效的栈遍历方法,使您能够对栈跟踪信息进行可过滤的访问,那么您将喜欢使用StackWalkerAPI。该 API 提供对调用栈的快速优化访问,实现对单个帧的延迟访问。
第 11 章“新工具和工具增强”,涵盖了十几种与现代 Java 平台相关的工具和工具增强。这些特色的变化将涵盖广泛的工具和 API 的更新,这些工具和 API 旨在简化 Java 开发,增强创建优化 Java 应用的能力。
第 12 章“并发增强”介绍了 Java 平台的并发增强。我们主要关注的是对反应式编程的支持,这是一种由Flow类 API 提供的并发增强。反应式编程最初是在 Java9 中发布的,它仍然是 Java10 和 Java11 的一个重要特性。
第 13 章“安全增强”介绍了最近对 JDK 进行的几个涉及安全性的更改,这些更改的大小并没有反映出它们的重要性。现代 Java 平台的安全性增强为开发人员提供了编写和维护比以前更安全的应用的能力。
第 14 章“命令行标志”,探讨了现代 Java 平台的一些变化,这些变化的共同主题是命令行标志。这些包括以下概念:统一 JVM 日志、编译器控制、诊断命令、堆分析代理、删除 JHAT、命令行标志参数验证、针对旧平台版本的编译,以及实验性的基于 Java 的 JIT 编译器。
第 15 章“Java 平台的附加增强”,重点介绍 Java 平台提供的附加工具的最佳实践。具体来说,本章介绍对 UTF-8、Unicode 支持、Linux/AArch64 端口、多分辨率图像和公共区域设置数据存储库的支持。
第 16 章“未来方向”概述了 Java 平台在 Java11 之外的未来发展。我们看一下 Java19.3 和 19.9 的计划内容,以及将来可能会看到哪些进一步的变化。我们首先简要介绍一下 JDK 增强程序(JEP)。
第 17 章“对 Java 平台的贡献”,讨论了 Java 社区和开发人员对 Java 平台的贡献方式。具体来说,本章涵盖了以下与 Java 社区相关的主题,如 Java 社区、参与 Java 用户组、Java 社区流程、Oracle 技术网络(OTN)以及撰写技术文章
充分利用这本书
我们鼓励您下载 Java11JDK,以便遵循本书中提供的示例。
下载示例代码文件
您可以从您的帐户下载本书的示例代码文件 www.packt.com。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
- 在登录或注册 www.packt.com。
- 选择“支持”选项卡。
- 点击代码下载和勘误表。
- 在搜索框中输入图书名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用最新版本的解压缩或解压缩文件夹:
- 用于 Windows 的 WinRAR/7-Zip
- Mac 的 Zipeg/iZip/UnRarX
- 用于 Linux 的 7-Zip/PeaZip
这本书的代码包也托管在 GitHub 上。如果代码有更新,它将在现有 GitHub 存储库中更新。
我们的丰富书籍和视频目录中还有其他代码包,可在这个页面上找到。看看他们!
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载。
使用的约定
这本书中使用了许多文本约定。
CodeInText:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。下面是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘装入。”
代码块设置如下:
try ( Scanner xmlScanner = new Scanner(new File(xmlFile)); {
while (xmlScanner.hasNext()) {
// read the xml document and perform needed operations
当我们希望提请您注意代码块的特定部分时,相关行或项以粗体显示:
public default void fastWalk() {
Scanner scanner = new Scanner(System.in);
System.out.println("Enter desired pacing: ");
任何命令行输入或输出的编写方式如下:
$ java --version
粗体:表示一个新术语、一个重要单词或屏幕上显示的单词。例如,菜单或对话框中的单词会像这样出现在文本中。下面是一个示例:“从管理面板中选择系统信息。”
警告或重要提示如下所示。
提示和窍门是这样出现的。
一、Java11 环境
在本章中,我们将探讨新实现的、基于时间的 Java 平台版本控制系统。我们将调查当前的 Java 环境,特别关注 Java9、Java10(18.3)和 Java11(18.9)引入的变化。我们的探索将包括对 Java9 的模块化、Javashell、外部进程控制、垃圾收集、Java 微基准线束(JMH)等的概述。对于 Java10,我们将重点介绍关键的更改,包括局部变量类型推断、Java 开发工具包(JDK)整合、垃圾收集、应用类数据共享(CDS)、根证书等等。最后,我们将探讨 Java11 中引入的更改,包括动态类文件常量、垃圾收集、Lambdas 的局部变量类型推断等等。
本章结束时,我们将学到的内容包括:
- 了解 Java 平台的新版本控制模型
- 了解 Java9 的重要性
- Java10 引入的变化带来的好处
- Java11 引入的变化带来的好处
技术要求
本章及后续章节主要介绍 Java11,Java 平台的标准版(SE)可从 Oracle 官方下载网站下载。
一个集成开发环境(IDE)包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
了解 Java 平台的新版本控制模型
Java 的第一个版本在 1996 年作为 Java1 发布。从那时起,出现了几个增量版本,每个版本都遵循一个特性驱动的版本模型。从 Java10 开始,Oracle 实现了一个新的、基于时间的发布模型。在本节中,我们将查看原始模型,以提供一个基础来说明 Java 平台是如何演变的,并查看新版本模型及其原因。
特性驱动的发布
在 1996 年 Java1 发布之后,随后的版本的命名为 1.1、1.2、1.3 和 1.4。随着 1.5 的发布,Java 平台被称为 Java5。在 Java6 发布之前,Java5 经常更新,随后是 Java7、Java8 和 Java9
下表提供了 Java9 之前的 Java 发行历史的压缩视图:
| 版本名称 | 版本 | 发布年份 | 代号 |
|---|---|---|---|
| Java1 | 1 | 1996 | Oak |
| Java1.1 | 1.1 | 1997 | (Abigail, Brutus, Chelsea) |
| Java2 | 1.2 | 1998 | Playground |
| Java3 | 1.3 | 2000 | Kestrel |
| Java4 | 1.4 | 2002 | Merlin |
| Java5 | 1.5 | 2004 | Tiger |
| Java6 | 1.6 | 2006 | Mustang |
| Java7 | 1.7 | 2011 | Dolphin |
| Java8 | 1.8 | 2014 | Spider |
| Java9 | 9 | 2017 | *不再使用代号 |
Java9 的发布是对 Java 平台和每个版本的编号方式的重大改变。在后 Java9 版本中,Oracle 决定放弃基于特性的模型,转而选择时间发布的模型
基于时间的发布
Java9 于 2017 年发布,2018 年计划发布两个版本。这些版本是 Java10 和 Java11。这些后 Java9 版本的版本号遵循YY.M格式。因此,随着 Java10 在 2018 年 3 月发布,版本号是 18.3。Java11 于 2018 年 9 月发布,版本号为 18.9。
新的基于时间的发布模型背后的一般前提是,发布的计划是可预测且频繁的。详情如下:
- 专题发布:每半年(每年 3 月、9 月)发布一次
- 更新发布:每季度发布一次
- 长期支持发布:每三年发布一次
从开发人员的角度来看,使用此模型可以获得巨大的收益。开发人员不再需要等待 Java 平台的发布。更重要的是,没有一个版本将代表对 Java9 平台的重大改变。
了解 Java9 的重要性
毫无疑问,作为 Jigsaw 项目的一部分开发的 Java 平台的模块化是 Java9 引入 Java 平台的最大变化。最初计划用于 Java8,但推迟了,Jigsaw 项目是 Java9 最终版本推迟的主要原因之一。Jigsaw 还为 Java 平台引入了一些显著的变化,这也是 Java9 被认为是主要版本的原因之一。我们将在后面的章节中详细探讨这些特性。
除了 Jigsaw 相关的 Java 增强建议之外,还有一长串在 Java9 中实现的其他增强。本节将探讨 Java9 中引入的最重要的特性,特别是:
- 拆解整体
- 使用 Java Shell
- 控制外部过程
- 使用 G1 提高性能
- 用 Java 微基准线束(JMH)测量性能
- 为 HTTP 2.0 做准备
- 包含反应式编程
拆解整体
多年来,Java 平台的工具不断发展和增加,使其成为一个巨大的整体。为了使平台更适合于嵌入式和移动设备,有必要发布精简版,如 Java 连接设备配置(CDC)和 Java 微型版(ME)。然而,对于 JDK 所提供的功能有不同需求的现代应用来说,这些方法并没有足够的灵活性。在这方面,对模块化系统的需求是一个至关重要的需求,不仅是为了解决 Java 工具的模块化(总的来说,HotSpot 运行时有 5000 多个 Java 类和 1500 多个 C++ 源文件,其中包含 250000 多行代码),而且还为开发人员提供了一种创建和管理的机制使用与 JDK 中使用的模块系统相同的模块化应用。Java8 提供了一种中间机制,使应用能够只使用整个 JDK 提供的 API 的一个子集,这种机制被命名为紧凑概要文件。事实上,紧凑的概要文件还为进一步的工作提供了基础,这些工作是为了打破 JDK 不同组件之间的依赖关系。为了在 Java 中实现模块系统,需要打破依赖关系。
模块系统本身以 Jigsaw 项目的名义开发,在此基础上形成了多个 Java 增强方案和一个目标 Java 规范请求(JSR376)。对 JDK 代码库进行了完整的重组,同时对 JDK 可分发映像进行了完整的重组。
对于是否应该采用一个现有的成熟的 Java 模块系统(比如 OSGi)作为 JDK 的一部分,而不是提供一个全新的模块系统,社区中存在着相当大的争议。但是,OSGi 以运行时行为为目标,比如模块依赖关系的解析、模块的安装、卸载、启动和停止(在 OSGi 中也称为 bundle)、自定义模块类加载器等等
OSGi 是指 OSGi 联盟,正式名称为开放服务网关倡议。OSGi 是 Java 平台模块化系统的开放标准。
然而,Jigsaw 项目的目标是编译时模块系统,在该系统中,依赖项的解析在编译应用时发生。此外,作为 JDK 的一部分安装和卸载一个模块,就不需要在编译过程中将它显式地包含为依赖项。此外,通过类加载器的现有层次结构(引导、扩展和系统类加载器)可以加载模块类。
Java 模块系统的其他好处包括增强的安全性和性能。通过将 JDK 和应用模块化为 Jigsaw 模块,开发人员能够在组件及其相应的域之间创建定义良好的边界。这种关注点的分离与平台的安全架构保持一致,并且是更好地利用资源的一个使能器
使用 Java Shell
很长一段时间以来,Java 编程语言中一直没有标准的 Shell 来试验新的语言特性或库,或者进行快速原型设计。如果您想这样做,您可以用一个main()方法编写一个测试应用,用javac编译它,然后运行它。这既可以在命令行中完成,也可以使用 JavaIDE 完成;但是,在这两种情况下,这并不像使用交互式 Shell 那样方便。
在 JDK9 中启动交互式 Shell 非常简单,只需运行以下命令(假设 JDK9 安装的bin目录位于当前路径中):
jshell
您可能会发现,在 Java 平台的早期还没有引入交互式 Shell,这有点令人费解,因为许多编程语言(如 Python、Ruby 和其他一些语言)在其最早的版本中已经附带了交互式 Shell。然而,直到 Java9,它才出现在优先特性列表中。javaShell 使用 JShellapi,它提供了启用表达式和代码段的自动补全或求值等功能。第 6 章“Java Shell 实验”,致力于讨论 Java Shell 的细节,让开发者充分利用。
控制外部进程
在 JDK9 之前,如果要创建 Java 进程并处理进程输入/输出,必须使用以下方法之一:
Runtime.getRuntime.exec()方法,它允许我们在单独的操作系统进程中执行命令。使用这种方法需要您获得一个java.lang.Process实例,在该实例上提供某些操作,以便管理外部流程。- 新的
java.lang.ProcessBuilder类,在与外部进程交互方面有更多的增强。您还需要创建一个java.lang.Process实例来表示外部进程。
这两种方法都是不灵活的,也不可移植的,因为外部进程执行的命令集高度依赖于操作系统。为了使特定的进程操作能够跨多个操作系统进行移植,还必须付出额外的努力。第 9 章“利用进程 API”开发了新的流程 API,为开发人员提供了创建和管理外部流程的知识。
使用 G1 提高性能
G1 垃圾收集器已经在 JDK7 中引入,现在在 JDK9 中默认启用。它针对具有多个处理核心和大量可用内存的系统。与以前的垃圾收集器相比,G1 有什么好处?它是如何实现这些改进的?是否需要手动调整,在什么情况下?关于 G1 的这些和其他几个问题将在第 7 章“利用默认的 G1 垃圾收集器”中讨论。
用 JMH 测量性能
在许多情况下,Java 应用可能会出现性能下降的问题,更严重的是缺乏性能测试,这些测试至少可以提供一组最低限度的性能保证来满足性能要求,而且,某些特性的性能不会随着时间的推移而下降。衡量 Java 应用的性能并非易事,特别是由于存在许多编译器和运行时优化,这些优化可能会影响性能统计。因此,为了提供更准确的性能度量,必须使用额外的度量,例如预热阶段和其他技巧。JMH 是一个框架,它包含了许多技术,以及一个方便的 API,可用于此目的。它不是一个新工具,但是包含在 Java9 的发行版中。如果您还没有将 JMH 添加到工具箱中,请阅读第 8 章、“使用 JMH 的微标记应用”,了解 JMH 在 Java 应用开发中的使用。
为 HTTP 2.0 做准备
HTTP2.0 是 HTTP1.1 协议的继承者,这个新版本的协议解决了前一个协议的一些限制和缺点。HTTP 2.0 以多种方式提高性能,并提供诸如在单个 TCP 连接中请求/响应多路复用、在服务器推送中发送响应、流控制和请求优先级等功能。Java 提供了可用于建立不安全 HTTP 1.1 连接的java.net.HttpURLConnection工具。然而,API 被认为难以维护,这一问题由于需要支持 HTTP 2.0 而变得更加复杂,因此引入了全新的客户端 API,以便通过 HTTP 2.0 或 Web 套接字协议建立连接。HTTP 2.0 客户端及其提供的功能,将在第 11 章、“新工具和工具增强”中介绍。
包含反应式编程
反应式编程是一种用于描述系统中变化传播的特定模式的范例。反应式不是 Java 本身构建的,但是可以使用第三方库(如 RxJava 或 projectreactor(Spring 框架的一部分))来建立反应式数据流。JDK9 还解决了对 API 的需求,该 API 通过为此提供java.util.concurrent.Flow类来帮助开发基于反应流思想构建的高响应性应用。Flow类以及 JDK9 中引入的其他相关更改将在第 12 章、“并发增强”中介绍。
受益于 Java10 带来的变化
Java10 于 2018 年 3 月发布,除了之前介绍的基于时间的版本控制之外,还有以下 11 个特性:
- 局部变量类型推断
- 将 JDK 森林整合到单个存储库中
- 垃圾收集接口
- G1 的并行完全垃圾收集器
- 应用类数据共享
- 线程本地握手
- 删除本机头生成工具(
javah) - 其他 Unicode 语言标记扩展
- 备用内存设备上的堆分配
- 基于 Java 的 JIT 编译器实验
- 根证书
本章将简要概述这些功能,随后的章节将更详细地介绍这些功能。
局部变量类型推断
从 Java10 开始,声明局部变量已经简化。开发人员不再需要包含本地变量类型的清单声明。这是使用新的var标识符完成的,如本示例所示:
var myList = new ArrayList<String>();
使用前面的代码,ArrayList<String>是推断出来的,所以我们不再需要使用ArrayList<String> myList = new ArrayList<String>();
局部变量类型推断在第 3 章、“Java11 基础”中介绍
将 JDK 森林整合到单个存储库中
在 Java10 之前,JDK 有八个存储库(CORBA、HotSpot、JDK、JAXP、JAX-WS、langtools、Nashorn 和 ROOT)。使用 Java10,这些存储库被整合到一个代码库中。值得注意的是,javafx 并不是这次整合的一部分。本课题将在第 2 章、第 11 章中进一步说明。
垃圾收集接口
Java10 带来了对垃圾收集过程的增强。新的垃圾收集器接口带来了改进,将在第 7 章“利用默认的 G1 垃圾收集器”中详细介绍。
G1 的并行完全垃圾收集器
在 Java10 中,G1 完全垃圾收集器是并行的。从 Java9 开始,G1 被设置为默认的垃圾收集器,因此这个更改具有特殊的意义。此更改将在第 7 章“利用默认的 G1 垃圾收集器”中详细说明。
应用类数据共享
类数据共享(CDS)已经扩展,以支持更快的应用启动和更小的占用空间。使用 cd,开发人员可以预先解析特定的类文件并将其存储在可共享的归档文件中。我们将在第 2 章“探索 Java11”中探讨 Java 平台的这种变化。
线程本地握手
使用 Java10 及更高版本,可以停止单个线程,而不必执行全局虚拟机安全点。我们将在第 3 章“Java11 基础”中充分探讨这一变化
删除本机头生成工具(javah)
为了将 Javah 工具从 JDK 中删除,进行了协调一致的工作。由于javac中提供的功能,因此此更改是有保证的。我们将在第 11 章、“新工具和工具增强”中详细说明这一变化。
附加 Unicode 语言标记扩展
Java 平台从 Java7 开始就支持语言标记。在 Java10 中,对java.util.Local和相关 API 进行了更改,以合并额外的 Unicode 语言标记。详见第 2 章、第 11 章。
备用内存设备上的堆分配
从 Java10 开始,热点虚拟机支持非 DRAM 内存设备。这将在第 3 章、“Java11 基础”中解释
基于 Java 的实验性 JIT 编译器
Java9 向我们介绍了一个基于 Java 的即时(JIT)编译器。此 JIT 编译器已为 Linux/x64 平台启用。这个实验编译器将在第 14 章、“命令行标志”中进一步探讨。
根证书
从 Java10 发布开始,JDK 中就有一组默认的证书颁发机构(CA)证书。这一变化及其好处将在第 3 章、“Java11 基础”中介绍
受益于 Java11 引入的变化
Java11 于 2018 年 9 月发布,具有以下四个特性:
- 动态类文件常量
- Epsilon 一个任意低开销的垃圾收集器
- 删除 JavaEE 和 CORBA 模块
- Lambda 参数的局部变量语法
本章将简要概述这些功能,随后的章节将更详细地介绍这些功能。
动态类文件常量
在 Java11 中,Java 类文件的文件格式被扩展为支持CONSTANT_Dynamic,它将创建委托给自举方法。这一变化将在第 3 章、“Java11 基础”中详细探讨
Epsilon–一个任意低开销的垃圾收集器
垃圾收集增强似乎是每个 Java 平台版本的一部分。Java11 包括一个不回收内存的被动垃圾收集器。我们将在第 7 章“利用默认的 G1 垃圾收集器”中探讨这一点。
删除 JavaEE 和 CORBA 模块
Java 企业版(JavaEE)和公共对象请求代理架构(CORBA)模块在 Java9 中被废弃,并从 Java11 开始从 Java 平台中移除。详见第 3 章、“Java11 基础”
Lambda 参数的局部变量语法
正如本章前面所讨论的,var标识符是在 Java10 中引入的。在最新版本 Java11 中,var可以用在隐式类型的 Lambda 表达式中。第 3 章“Java11 基础”介绍了var标识符的使用
总结
在本章中,我们探讨了新实现的、基于时间的 Java 平台版本控制系统。我们还从较高的层次了解了 Java9、10 和 11 中引入的更改(分别称为 9、18.3 和 18.9 版本)。Java9 最重要的变化是基于 Jigsaw 项目的模块化,包括关注 Javashell、控制外部进程、垃圾收集、JHM 等的其他变化。讨论了 Java10 的关键特性,包括局部变量类型推断、JDK 整合、垃圾收集、应用 CD、根证书等等。Java11 中引入的更改包括动态类文件常量、垃圾收集、Lambdas 的局部变量类型推断等等。
在下一章中,我们将研究 Java 平台中引入的几个内部更改,包括来自 Java9、10 和 11 的更改。
问题
- 2019 年第一个 Java 版本会是什么?
- 新的 Java 基于时间的发布模型的主要优点是什么?
- JDK9 对 Java 平台最重要的改变是什么?
- Java11 删除了什么:CORBA、Lambda 还是 G1?
- CD 支持更快的启动还是更高效的垃圾收集?
- 什么是 Epsilon?
var是数据类型、标识符、保留字还是关键字?- 哪个 Java 版本向 Java 平台引入了根证书?
- 哪个版本包括对垃圾收集的增强?
- Java 中默认的垃圾收集器是什么?
进一步阅读
本调查章节对 Java 平台的最新变化进行了粗略的介绍。如果您不熟悉其中任何一个概念,请考虑使用以下一个或多个资源来复习 Java 知识:
二、探索 Java11
在上一章中,我们探讨了新实现的 Java 平台基于时间的版本控制系统。我们还从高层次了解了 Java9、10 和 11 中引入的更改,这些更改分别称为 9、18.3 和 18.9 版本。Java9 最重要的变化是引入了基于 Jigsaw 项目的模块化,包括关注 Javashell、控制外部进程、垃圾收集、JHM 等的其他变化。介绍了 Java10 的主要特性,包括局部变量类型推断、JDK 合并、垃圾收集、应用类数据共享(CDS)、根证书等。Java11 中引入的更改包括动态类文件常量、垃圾收集、Lambda 的局部变量类型推断等等。
在本章中,我们将介绍几个引入 Java 平台的内部更改,包括来自 Java9、10 和 11 的更改。Java9 是 Java 平台的主要版本;Java10 和 11 是定时版本。总的来说,这些版本包含了大量的内部更改,为 Java 开发人员提供了一系列新的可能性,有些源于开发人员的请求,有些源于 Oracle 的增强
在本章中,我们将回顾 29 个最重要的变化。每个变更都与一个 JDK 增强方案(JEP)相关。JEP 索引并存放在这个页面。您可以访问此链接以获取有关每个 JEP 的更多信息。
JEP 计划是 Oracle 支持开源、开放创新和开放标准的一部分。虽然可以找到其他开源 Java 项目,但 OpenJDK 是 Oracle 唯一支持的项目。
在本章中,我们将介绍以下内容:
- 改进的争用锁
- 分段代码缓存
- 智能 Java 编译,第二阶段【JEP199】
- 解决 Lint 和 Doclint 警告【JEP212】
- Javac 的分层属性【JEP215】
- 注解管道 2.0【JEP217】
- 新版本字符串方案
- 自动生成运行时编译器测试【JEP233】
- 测试 Javac【JEP235】生成的类文件属性
- 在 CD 档案中存储内部字符串【JEP250】
- 为模块化准备 JavaFXUI 控件和 CSS API【JEP253】
- 紧凑字符串
- 将选定的 Xerces 2.11.0 更新合并到 JAXP【JEP255】
- 将 JavaFX/Media 更新为 GStreamer 的更新版本【JEP257】
- HarfBuzz 字体布局引擎
- Windows 和 Linux 上的 HiDPI 图形【JEP263】
- Marlin 图形渲染器
- Unicode 8.0.0[JEP267 和 JEP314]
- 临界段的预留栈区【JEP270】
- 语言定义对象模型的动态链接
- G1 中大型对象的附加试验【JEP278】
- 改进测试失败的故障排除
- 优化字符串连接
- Hotspot C++ 单元测试框架【JEP281】
- 在 Linux 上启用 GTK3【JEP283】
- 新 Hotspot 构建系统
- 将 JDF 森林整合到单个存储库中【JEP296】
技术要求
本章及后续章节以 Java11 为特色,Java 平台的标准版(SE)可从 Oracle 官方下载网站的链接下载。
一个集成开发环境(IDE)包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
本章的源代码可以在 GitHub 的 URL上找到。
改进的争用锁
JVM 将堆空间用于类和对象。每当我们创建一个对象时,JVM 就会在堆上分配内存。这有助于促进 Java 的垃圾收集,垃圾收集释放以前用来保存不再有内存引用的对象的内存。Java 栈内存有点不同,通常比堆内存小得多
JVM 在管理由多个线程共享的数据区域方面做得很好。它将监视器与每个对象和类相关联;这些监视器具有在任何时候由单个线程控制的锁。这些由 JVM 控制的锁本质上是给控制线程对象的监视器?当一个线程在一个队列中等待一个当前被锁定的对象时,它就被认为是在争夺这个锁。下图显示了此争用的高级视图:

正如您在前面的图中所看到的,任何正在等待的线程在被释放之前都不能使用锁定的对象。
改进目标
JEP143 的总体目标是提高 JVM 如何在锁定的 Java 对象监视器上管理争用的总体性能。对争用锁的改进都是 JVM 内部的,不需要任何开发人员操作就可以从中获益。总体改进目标与更快的操作相关。其中包括:
- 更快的监视器输入
- 更快的监视器退出
- 更快的通知
通知是当对象的锁定状态改变时调用的notify()和notifyAll()操作。测试这种改进并不是一件容易完成的事情。任何级别的更高的效率都是值得欢迎的,因此这一改进是值得我们感谢的。
分段代码缓存
Java 的分段代码缓存升级已经完成,结果是执行速度更快、效率更高。这一变化的核心是将代码缓存分割为三个不同的段:非方法段、概要代码段和非概要代码段。
代码缓存是 JVM 存储生成的本机代码的内存区域。
前面提到的每个代码缓存段都将保存特定类型的编译代码。如下图所示,代码堆区域按编译代码的类型进行分段:

内存分配
包含非方法代码的代码堆用于 JVM 内部代码,由 3MB 固定内存块组成。其余的代码缓存内存平均分配给已分析代码和未分析代码段。您可以通过命令行命令对此进行控制。
以下命令可用于定义非方法编译代码的代码堆大小:
-XX:NonMethodCodeHeapSize
以下命令可用于定义已分析编译方法的代码堆大小:
-XX:ProfiledCodeHeapSize
以下命令可用于定义非概要编译方法的代码堆大小:
-XX:NonProfiledCodeHeapSize
这个特性当然可以提高 Java 应用的效率。它还会影响使用代码缓存的其他进程。
智能 Java 编译
所有 Java 开发人员都将熟悉将源代码编译成字节码的工具,JVM 使用它来运行 Java 程序。智能 Java 编译,也称为智能 Javac 和sjavac,在javac进程周围添加了一个智能包装器。sjavac增加的核心改进可能是只重新编译必要的代码。在此上下文中,必要的代码是自上一个编译周期以来更改的代码。
如果开发人员只在小项目上工作,这种增强可能不会让他们感到兴奋。但是,考虑一下,当您不断地为中大型项目重新编译代码时,在效率方面的巨大收益。开发人员节省的时间足以让他们接受 JEP199。
这将如何改变编译代码的方式?它可能不会,至少现在不会,Javac 仍然是默认的编译器。尽管sjavac提供了增量构建的效率,但 Oracle 认为它没有足够的稳定性来成为标准编译工作流程的一部分。
解决 Lint 和 Doclint 警告
Lint 和 Doclint 是向javac报告警告的来源。我们来看看每一个:
- Lint 分析
javac的字节码和源代码。Lint 的目标是识别所分析代码中的安全漏洞。Lint 还可以深入了解可伸缩性和线程锁定问题。Lint 还有更多的功能,其总体目的是节省开发人员的时间。
- Doclint 与 Lint 类似,是针对
javadoc的。Lint 和 Doclint 都报告编译过程中的错误和警告。这些警告的解决是 JEP212 的重点。使用核心库时,不应出现任何警告。这种思维方式导致了 JEP212,它已经在 Java9 中得到了解决和实现。
Lint 和 Doclint 警告的综合列表可以在 JDK 错误系统(JBS)中查看,可在这个页面中获得。
Javac 的分层属性
Javac 的类型检查已经简化了,让我们首先回顾一下 Java8 中的类型检查是如何工作的,然后我们将探讨现代 Java 平台中的变化。
在 Java8 中,poly 表达式的类型检查由推测属性工具处理
推测属性是一种类型检查方法,作为javac编译过程的一部分。它有很大的处理开销。
使用推测属性方法进行类型检查是准确的,但缺乏效率。这些检查包括参数位置,在递归、多态、嵌套循环和 Lambda 表达式中进行测试时,速度会以指数级的速度减慢。因此,更新的目的是更改类型检查模式以创建更快的结果。结果本身并不是不准确的推测归因,他们只是没有迅速产生。
Java9-11 中提供的新方法使用了分层属性工具。此工具实现了一种分层方法,用于对所有方法调用的参数表达式进行类型检查。还为方法重写设置了权限。为了使此新架构正常工作,将为以下列出的每种类型的方法参数创建新的结构类型:
- Lambda 表达式
- 多边形表达式
- 常规方法调用
- 方法引用
- 菱形实例创建表达式
对javac的修改比本节强调的更为复杂。对开发人员来说,除了效率更高和节省时间之外,没有什么直接的影响。
注解管道 2.0
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
}
那么,Java9、10 和 11 中发生了什么变化?要回答这个问题,我们需要回顾一下 Java8 引入的几个影响 Java 注解的更改:
- Lambda 表达式
- 重复注解
- Java 类型注解
这些与 Java8 相关的更改影响了 Java 注解,但并没有改变javac处理它们的方式。有一些硬编码的解决方案允许javac处理新的注解,但它们效率不高。此外,这种类型的编码(硬编码解决方法)很难维护。
因此,JEP217 专注于重构javac注解管道。这种重构都是javac内部的,所以对开发人员来说应该不明显。
新版本字符串方案
在 Java9 之前,版本号没有遵循行业标准的版本控制语义版本控制。例如,最后四个 JDK8 版本如下:
- Java SE 8 更新 144
- Java SE 8 更新 151
- Java SE 8 更新 152
- Java SE 8 更新 161
- Java SE 8 更新 162
语义版本控制使用主要、次要、补丁(0.0.0)模式,如下所示:
- 主要等同于不向后兼容的新 API 更改
- 次要是添加向后兼容的功能的情况
- 补丁是指错误修复或向后兼容的小改动
Oracle 从 Java9 开始就支持语义版本控制。对于 Java,Java 版本号的前三个元素将使用主次安全模式:
- 主要:由一组重要的新特性组成的主要版本
- 次要:向后兼容的修订和错误修复
- 安全:被认为是提高安全性的关键修复
Java9 有三个版本:初始版本和两个更新。下面列出的版本演示了主要的次要安全模式:
- Java SE 9
- Java SE 9.0.1
- Java SE 9.0.4
如第 1 章、“Java11 场景”所述,在 Java9 之后的版本将遵循的时间发布模式年月日。使用该模式,Java9 之后的四个版本如下:
- Java SE 18.3(2018 年 3 月)
- Java SE 18.9(2018 年 9 月)
- Java SE 19.3(2019 年 3 月)
- Java SE 19.9(2019 年 9 月)
自动生成运行时编译器测试
Java 可以说是最常用的编程语言,并且驻留在越来越多样化的平台上。这加剧了以有效方式运行目标编译器测试的问题。新的 Java 平台包括一个自动化运行时编译器测试的工具。
这个新工具首先生成一组随机的 Java 源代码和/或字节码。生成的代码将具有三个关键特征:
- 它在语法上是正确的
- 它在语义上是正确的
- 它将使用一个随机种子,允许重用相同的随机生成的代码
随机生成的源代码将保存在以下目录中:
hotspot/test/testlibrary/jit-tester
这些测试用例将被存储起来以供以后重用。它们可以从j-treg目录或工具的 makefile 运行。重新运行保存的测试的好处之一是测试系统的稳定性。
测试 Javac 生成的类文件属性
缺乏或不足以为类文件属性创建测试的能力是确保javac完全正确地创建类文件属性的动力。这表明,即使某些属性没有被类文件使用,所有类文件都应该生成一组完整的属性。还需要有一种方法来测试类文件是否根据文件的属性正确创建
在 Java9 之前,没有测试类文件属性的方法。运行类并测试代码以获得预期的或预期的结果是测试javac生成的类文件最常用的方法。这种技术无法通过测试来验证文件的属性。
JVM 使用的类文件属性有三类:可选属性和 JVM 不使用的属性。
JVM 使用的属性包括:
BootstrapMethodsCodeConstantValueExceptionsStackMapTable
可选属性包括:
DeprecatedLineNumberTableLocalVariableTableLocalVariableTypeTableSourceDebugExtensionSourceFile
JVM 未使用的属性包括:
AnnotationDefaultEnclosingMethodInnerClassesMethodParametersRuntimeInvisibleAnnotationsRuntimeInvisibleParameterAnnotationsRuntimeInvisibleTypeAnnotationsRuntimeVisibleAnnotationsRuntimeVisibleParameterAnnotationsRuntimeVisibleTypeAnnotationsSignatureSynthetic
在类数据共享档案中存储内部字符串
在 Java5 到 Java5 中,存储字符串并从 CDS 存档中访问字符串的方法效率低下,非常耗时,而且浪费了内存。下图说明了 Java 在 Java9 之前将内部字符串存储在 CD 存档中的方法:

低效率源于存储模式。当 CDS 工具将类转储到共享存档文件中时,这一点尤为明显。包含CONSTANT_String项的常量池具有 UTF-8 字符串表示。
UTF-8 是一种 8 位可变长度字符编码标准。
问题
在 Java9 之前使用 UTF-8 时,字符串必须转换为字符串对象,即java.lang.String类的实例。这种转换是按需进行的,这通常会导致系统速度变慢和不必要的内存使用。处理时间非常短,但内存使用过多。一个内部字符串中的每个字符都需要至少 3 个字节的内存,甚至更多。
一个相关的问题是,并非所有 JVM 进程都可以访问存储的字符串。
Java9 解决方案
CDS 存档从 Java9 开始,在堆上为字符串分配特定的空间。该过程如下图所示:

使用共享字符串表、哈希表和重复数据消除映射字符串空间。
数据去重是一种数据压缩技术,可消除档案中的重复信息。
Java10 的改进
Java9 引入了更高效的 cd,Java9 进一步改进了这个特性,特别是支持将应用类添加到共享存档中。JEP310 应用 cd 的目的不是为了使归档文件膨胀、启动时间变慢或消耗超过需要的内存。尽管如此,如果不对 CDS 采取有目的的方法,这些结果是可能的
我们对 CDS 存档使用三个步骤:确定要包含的类、创建存档和使用存档:
- 类的确定
- AppCD 存档创建
- 使用 AppCD 存档
让我们检查一下每一步的细节。
类的确定
使用 cd 的最佳实践是只归档所使用的类。这将有助于防止档案不必要地膨胀。我们可以使用以下命令行和标志来确定加载了哪些类:
java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=ch2.lst - cp cp2.jar Chapter2
AppCD 存档创建
一旦我们知道加载了哪些类,我们就可以创建 AppCDS 存档。以下是要使用的命令行和标志选项:
java -Xshare:dump -XX:+UseApsCDS \
-XX:SharedClassListFile=ch2.lst \
-XX:SharedArchiveFile=ch2.jsa -cp ch2.jar
使用 AppCD 存档
为了使用 AppCDS 存档,我们发出-Xshare:on命令行选项,如下所示:
java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=ch2.jsa -cp ch2.jar Chapter2
为模块化准备 JavaFXUI 控件和级联样式表 API
JavaFX 是一组允许设计和开发富媒体图形用户界面的包。JavaFX 应用为开发人员提供了一个很好的 API,用于为应用创建一致的接口。级联样式表(CSS)可用于定制接口。JavaFX 的一个优点是编程和接口设计的任务可以很容易地分开。
JavaFX 概述
JavaFX 包含一个很棒的可视化脚本工具场景构建器,它允许您使用拖放和属性设置来创建图形用户界面。场景生成器生成 IDE 使用的必要 FXML 文件,例如 NetBeans。
以下是使用场景生成器创建的示例 UI:

下面是场景生成器创建的 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"
xmlns:fx="http://javafx.com/fxml/1"
>
<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>
对 Java9、10 和 11 的影响
在 Java9 之前,JavaFX 控件和 CSS 功能只能通过与内部 API 接口提供给开发人员。Java9 的模块化使得内部 API 无法访问。因此,创建 JEP253 是为了定义公共 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 |
HpyerLinkSkin |
LabelSkin |
LabeledSkinBase |
ListCellSkin |
ListViewSkin |
MenuBarSkin |
MenuButtonSkin |
MenuButtonSkinBase |
NestedTableColumnHeader |
PaginationSkin |
ProgressBarSkin |
ProgressIndicatorSkin |
RadioButtonSkin |
ScrollBarSkin |
ScrollPanelSkin |
SeparatorSkin |
SliderSkin |
SpinnerSkin |
SplitMenuButtonSkin |
SplitPaneSkin |
TabPaneSkin |
TableCellSkin |
TableCellSkinBase |
TableColumnHeader |
TableHeaderRow |
TableHeaderSkin |
TabelRowSkinBase |
TableViewSkin |
TableViewSkinBase |
TextAreaSkin |
TextFieldSkin |
TextInputControlSkin |
TitledPaneSkin |
ToggleButtonSkin |
ToolBarSkin |
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 SelectorCssError.java:public class CssErrorDeclaration.java:final public class DeclarationRule.java:final public class RuleSelector.java:abstract public class SelectorSimpleSelector.java:final public class SimpleSelector extends SelectorSize.java:final public class SizeStyle.java:final public class StyleStylesheet.java:public class StylesheetCssParser.java:final public class CssParser
紧凑字符串
字符串数据类型几乎是每个 Java 应用的重要组成部分,在 Java9 之前,字符串数据存储为一个数组chars。这要求每个char有 16 位。确定大多数字符串对象只能用 8 位或 1 字节的存储空间来存储。这是因为大多数字符串都由拉丁 1 字符组成。
拉丁 1 字符是指国际标准化组织建立的拉丁 1 字符集。字符集由字符编码的单字节集组成。
从 Java9 开始,字符串现在在内部用一个byte数组表示,还有一个用于编码引用的标志字段。
将选定的 Xerces 2.11.0 更新合并到 JAXP 中
Xerces 是一个用于在 Java 中解析 XML 的库。它在 2010 年末被更新为 2.11.0,JAXP 也被更新为包含 Xerces2.11.0 中的更改。
JAXP 是 Java 用于 XML 处理的 API。
在 Java9 之前,JDK 关于 XML 处理的最新更新是基于 Xerces2.7.1 的,JDK7 在 Xerces2.10.0 的基础上有一些额外的变化。Java 现在对基于 Xerces2.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 2.0.2 的简单 API
-
Java API for XML Processing(JAXP)1.4
-
XML 1.0 流 API
-
XML 模式 1.0
-
XML 模式 1.1
-
XML 模式定义语言
JDK 已更新为包括以下 Xerces 2.11.0 类别:
- 目录分解器
- 数据类型
- 文档对象模型级别 3
- XML 架构验证
- XPointer
JAXP 的公共 API 在 Java9、10 或 11 中没有改变。
将 JavaFX/Media 更新为 GStreamer 的更新版本
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 是一个多媒体处理框架,可用于构建系统,该系统接收多种不同格式的媒体,并在处理后以选定的格式导出它们。
对现代 Java 平台的更新确保了 JavaFX/Media 被更新为包括 GStreamer 的最新版本,以保证稳定性、性能和安全性。
HarfBuzz 字体布局引擎
在 Java9 之前,布局引擎用于处理字体的复杂性,特别是那些具有超出常用拉丁字体的呈现行为的字体。Java 使用统一客户端接口(也称为 ICU)作为事实上的文本呈现工具。ICU 布局引擎已经贬值,在 Java9 中,已经被 HarfBuzz 字体布局引擎所取代。
HarfBuzz 是一个 OpenType 文本呈现引擎。这种类型的布局引擎的特点是提供脚本感知代码,以帮助确保文本按所需布局。
OpenType 是一个 HTML 格式的字体格式规范。
从 ICU 布局引擎向 HarfBuzz 字体布局引擎转变的动力是 IBM 决定停止支持 ICU 布局引擎。因此,JDK 被更新为包含 HarfBuzz 字体布局引擎。
Windows 和 Linux 下的 HiDPI 图形
为了确保屏幕组件相对于显示器像素密度的清晰度,我们做出了一致的努力。以下术语与此工作相关,并随附所列描述性信息一起提供:
- DPI 感知应用:一种能够根据显示器的特定像素密度检测和缩放图像的应用。
- DPI 非感知应用:不尝试检测和缩放显示器特定像素密度的图像的应用。
- HiDPI 图形:每英寸高点图形。
- 视网膜显示器:这个术语是由苹果公司创建的,指像素密度至少为每英寸 300 像素的显示器。向用户显示图形(包括图像和图形用户界面组件)通常是最重要的性能。以高质量显示此图像可能有些问题。计算机显示器 DPIs 的变化很大。开发显示器有三种基本方法:
- 开发应用时不考虑潜在的不同显示尺寸。换句话说,创建一个 DPI 应用。
- 开发一个支持 DPI 的应用,有选择地使用给定显示的预渲染图像大小。
- 开发一个支持 DPI 的应用,该应用可以根据运行应用的特定显示适当地上下缩放图像。
显然,前两种方法有问题,原因不同。对于第一种方法,不考虑用户体验。当然,如果应用是为一个没有预期像素密度变化的非常特定的显示而开发的,那么这种方法是可行的。
第二种方法需要在设计和开发端进行大量工作,以确保以编程方式创建和实现每个预期显示密度的图像。除了大量的工作之外,应用大小将不必要地增加,并且新的和不同的像素密度将不被考虑。
第三种方法是创建具有 DPI 意识的应用,该应用具有高效和有效的扩展功能。这种方法工作得很好,已经在 Mac 视网膜显示器上得到了验证
在 Java9 之前,MacOSX 已经在 Java 中实现了自动伸缩和调整大小。这个功能是在 Windows 和 Linux 操作系统的 Java9 中添加的。
Marlin 图形渲染器
在 Java2dAPI 中,双鱼座图形光栅化器已经被 Marlin 图形渲染器所取代。此 API 用于绘制 2D 图形和动画。
我们的目标是用一个光栅化器/渲染器来代替双鱼座,这个光栅化器/渲染器效率更高,而且没有任何质量损失。这个目标是在 Java9 中实现的。一个预期的附带好处是包括一个开发人员可访问的 API。以前,与 AWT 和 Java2d 的接口是内部的。
Unicode 8.0.0 标准
Unicode 8.0.0 于 2015 年 6 月 17 日发布。Java 的相关 API 已更新为支持 Unicode 8.0.0。
Unicode 8.0.0 中的新功能
Unicode 8.0.0 增加了近 8000 个字符。以下是此次发布的亮点:
- 泰阿洪语的阿洪语脚本(印度)
- 阿尔维语,泰米尔语(阿拉伯语)
- 切罗基符号
- 中日韩统一象形文字
- 表情符号以及肉色符号修饰符
- 格鲁吉亚拉里货币符号
- lk 语言(乌干达)
- 库兰戈语(科特迪瓦)
Java9 中的类更新
为了完全符合新的 Unicode 标准,更新了几个 Java 类。为使 Java9 符合新的 Unicode 标准,更新了以下列出的类:
java.awt.font.NumericShaperjava.lang.Characterjava.lang.Stringjava.text.Bidijava.text.BreakIteratorjava.text.Normalizer
临界段的预留栈区
在执行关键部分期间,由栈溢出引起的问题得到了缓解。这种缓解措施的形式是保留额外的线程栈空间。
Java9 之前的情况
当 JVM 被要求在栈空间不足且没有分配额外空间权限的线程中执行数据计算时,JVM 抛出StackOverflowError。这是一个异步异常。JVM 还可以在调用方法时同步抛出StackOverflowError异常。
调用方法时,将使用内部进程报告栈溢出。虽然当前模式足以报告错误,但调用应用没有空间轻松地从错误中恢复。这不仅会给开发者和用户带来麻烦。如果StackOverflowError是在关键的计算操作期间抛出的,则数据可能已损坏,从而导致其他问题。
虽然不是这些问题的唯一原因,ReentrantLock类的锁的有效状态是导致不良结果的常见原因。这个问题在 Java7 中很明显,因为ConcurrentHashMap代码实现了ReentrantLock类。为 Java8 修改了ConcurrentHashMap代码,但是ReentrantLock类的任何实现仍然存在问题。类似的问题不仅仅存在于ReentrantLock类的使用上。
下图概括介绍了StackOverflowError问题:

在下一节中,我们将看看 Java9 是如何解决这个问题的。
Java9 中的新功能
随着现代 Java 平台的变化,一个关键的部分会自动地被赋予额外的空间,这样它就可以完成它的执行而不受StackOverflowError的影响,这是基于额外的空间分配需求很小。对 JVM 进行了必要的更改以允许此功能。
当关键部分正在执行时,JVM 实际上会延迟StackOverflowError,或者至少尝试延迟。为了利用这个新模式,必须用以下内容对方法进行注解:
jdk.internal.vm.annotation.ReservedStackAccess
当一个方法有这个注解并且存在一个StackOverflowError条件时,就授予对保留内存空间的临时访问权。新流程在高抽象层次上呈现如下:

语言定义对象模型的动态链接
Java 互操作性得到了增强。对 JDK 进行了必要的更改,以允许来自多种语言的运行时链接器在单个 JVM 实例中共存。正如您所期望的那样,此更改适用于高级操作。相关高级操作的一个示例是使用诸如访问器和变异器之类的元素读取或写入属性。
高级操作适用于未知类型的对象。它们可以通过INVOKEDYNAMIC指令调用。下面是一个在编译时对象类型未知时调用对象属性的示例:
INVOKEDYNAMIC "dyn:getProp:age"
概念证明
Nashorn 是一个轻量级、高性能的 JavaScript 运行时,它允许在 Java 应用中嵌入 JavaScript。它是为 Java8 创建的,并取代了以前基于 MozillaRhino 的 JavaScript 脚本引擎。Nashorn 已经有了这个功能。它提供对任何未知类型的对象(如obj.something)的高级操作之间的链接,其中它产生以下结果:
INVOKEDYNAMIC "dyn.getProp.something"
动态链接器启动并在可能的情况下提供适当的实现。
G1 中大型对象的附加试验
Java 平台长期以来最受欢迎的特性之一是幕后垃圾收集。改进的目标是为庞大的对象创建额外的白盒测试,作为 G1 垃圾收集器的一个特性。
白盒测试是用于查询 JVM 内部的 API。 白盒测试 API 是在 Java7 中引入的,并在 Java8 和 Java9 中进行了升级。
G1 垃圾收集器工作得非常好,但仍有提高效率的空间。G1 垃圾收集器的工作方式是首先将堆划分为大小相等的区域,如下所示:

G1 垃圾收集器的问题是如何处理庞大的对象。
在垃圾收集上下文中,庞大的对象是占用堆上多个区域的任何对象。
庞大对象的问题是,如果它们占用了堆上某个区域的任何部分,那么剩余的空间就无法分配给其他对象。在 Java9 中,白盒 API 扩展了四种新方法:
- 方法,其目的是阻止完全垃圾收集并启动并发标记。
- 可以访问单个 G1 垃圾收集堆区域的方法。对这些区域的访问包括属性读取,例如区域的当前状态。
- 直接访问 G1 垃圾收集内部变量的方法。
- 方法,这些方法可以确定堆上是否存在大量对象,如果存在,则位于哪些区域。
改进测试失败的故障排除
Java 中添加了额外的功能来自动收集信息,以支持测试失败和超时的故障排除。在测试期间收集现成的诊断信息,为开发人员和工程师的日志和其他输出提供更高的保真度。
测试中有两种基本类型的信息:
- 环境
- 进程
每种类型的信息将在下一节中描述。
环境信息
在运行测试时,测试环境信息对于故障排除工作非常重要。这些信息包括:
- CPU 负载
- 磁盘空间
- I/O 负载
- 内存空间
- 打开的文件
- 打开的套接字
- 正在运行的进程
- 系统事件
- 系统消息
Java 进程信息
在测试过程中也有与 Java 进程直接相关的信息。其中包括:
- C 堆
- 堆转储
- 小型转储
- 堆统计信息
- Java 栈
关于这个概念的更多信息,请阅读 JDK 的回归测试工具(jtreg)。
优化字符串连接
在 Java9 之前,字符串连接由javac翻译成StringBuilder : : append链。这是一种次优的翻译方法,通常需要预先确定。
增强更改了由javac生成的字符串连接字节码序列,因此它使用INVOKEDYNAMIC调用。增强的目的是增加优化并支持将来的优化,而不需要重新格式化javac的字节码。
有关INVOKEDYNAMIC的更多信息,请参见 JEP276。
使用INVOKEDYAMIC调用java.lang.invoke.StringConcatFactory允许我们使用类似于 Lambda 表达式的方法,而不是使用StringBuilder的逐步过程。这样可以更有效地处理字符串连接。
HotSpot C++ 单元测试框架
HotSpot 是 JVM 的名称。此 Java 增强旨在支持 JVM 的 C++ 单元测试的开发。以下是此增强功能的部分非优先目标列表:
- 命令行测试
- 创建适当的文档
- 调试编译目标
- 框架弹性
- IDE 支持
- 单个和独立单元测试
- 个性化测试结果
- 与现有基础设施集成
- 内部测试支持
- 正例和负例检测
- 短执行时间测试
- 支持所有 JDK9 构建平台
- 测试编译目标
- 测试排除
- 测试分组
- 需要初始化 JVM 的测试
- 测试与源代码共存
- 平台相关代码的测试
- 编写和执行单元测试(针对类和方法)
这种增强是扩展性不断增强的证据。
在 Linux 上启用 GTK3
GTK+,正式称为 GIMP 工具箱,是一种用于创建图形用户界面的跨平台工具。该工具由可通过其 API 访问的小部件组成,Java 的增强功能确保在 Linux 上开发带有图形组件的 Java 应用时支持 GTK2 和 GTK3。该实现支持使用 JavaFX、AWT 和 Swing 的 Java 应用。
我们可以使用 JavaFX、AWT 和 Swing 创建 Java 图形应用。下面的表格总结了这三种方法与 GTK(Java9 之前)的关系:
| 方法 | 备注 |
|---|---|
| JavaFX | 使用动态 GTK 函数查找 |
| 通过 JFXPanel 与 AWT 和 Swing 交互 | |
| 使用 AWT 打印功能 | |
| AWT | 使用动态 GTK 函数查找 |
| Swing | 使用动态 GTK 函数查找 |
那么,实现这一增强需要进行哪些更改?对于 JavaFX,更改了三个具体内容:
- GTK 2 和 GTK 3 都增加了自动测试
- 添加了动态加载 GTK2 的功能
- 为 GTK 3 添加了支持
对于 AWT 和 Swing,实现了以下更改:
- GTK 2 和 GTK 3 都增加了自动测试
AwtRobot迁移到 GTK3- 为 GTK 3 更新了
FileChooserDilaog - 添加了动态加载 GTK3 的功能
- Swing GTK LnF 经过修改以支持 GTK 3
Swing GTK LnF 是 Swing GTK look and feel 的缩写。
新 HotSpot 构建系统
在 Java9-11 之前使用的 Java 平台是一个充满重复代码、冗余和其他低效的构建系统。构建系统已经为基于 buildinfra 框架的现代 Java 平台重新设计。在这种情况下,infra 是 infrastructure 的缩写。这个增强的首要目标是将构建系统升级到一个简化的系统。
具体目标包括:
- 利用现有构建系统
- 创建可维护代码
- 最小化重复代码
- 简化
- 支持未来的增强功能
您可以通过以下链接了解更多关于 Oracle 基础架构框架的信息。
将 JDF 森林整合到单个存储库中
Java9 平台由八个不同的存储库组成,如下图所示。在 Java10 中,所有这些存储库都合并到一个存储库中:

存储库整合有助于简化开发。此外,它还增加了维护和更新 Java 平台的容易性。
总结
在本章中,我们介绍了 Java9、10 和 11 引入的 Java 平台的一些令人印象深刻的新特性。我们关注于javac、JDK 库和各种测试套件。内存管理的改进,包括堆空间效率、内存分配和改进的垃圾收集,代表了一组强大的 Java 平台增强功能。关于提高效率的汇编过程的变化是我们这一章的一部分。我们还介绍了一些重要的改进,如编译过程、类型测试、注解和自动运行时编译器测试。
在下一章中,我们将介绍 Java9、10 和 11 中引入的几个小的语言增强。
问题
- 什么是乐观锁?
- 什么是代码缓存?
- 用于定义已分析编译方法的代码堆大小的命令行代码是什么?
- 警告上下文中的 Lint 和 Doclint 是什么?
- 自动生成运行时编译器测试时使用的目录是什么?
- 在确定 CDS 类时,
-Xshare命令行选项使用了什么标志? - 场景生成器生成的文件扩展名是什么?
- 在 Java9 之前,字符串数据是如何存储的?
- 从 Java9 开始,字符串数据是如何存储的?
- 什么是 OpenType?
进一步阅读
这里列出的书籍也可以作为电子书提供,它们将帮助您深入了解 Java9 和 JavaFX:
三、Java11 基础
在最后一章中,我们介绍了 Java9、10 和 11 引入的 Java 平台的一些令人印象深刻的新特性。我们关注 Javac、JDK 库和各种测试套件。内存管理的改进,包括堆空间效率、内存分配和改进的垃圾收集,代表了一组强大的 Java 平台增强功能。关于提高效率的汇编过程的变化是我们这一章的一部分。我们还介绍了一些重要的改进,例如有关编译过程、类型测试、注解和自动运行时编译器测试的改进。
本章介绍对 Java 平台的一些更改,这些更改会影响变量处理器、导入语句、对 Coin 项目的改进、局部变量类型推断、根证书、动态类文件常量等等。这些表示对 Java 语言本身的更改。
我们将在本章介绍的具体主题如下:
- 变量处理器
import语句废弃警告- Coin 项目
import语句处理- 推断局部变量
- 线程本地握手
- 备用内存设备上的堆分配
- 根证书
- 动态类文件常量
- 删除 JavaEE 和 CORBA 模块
技术要求
本章及后续章节主要介绍 Java11,Java 平台的标准版(SE)可从 Oracle 官方网站下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
本章的源代码可以在 GitHub 上找到。
使用变量处理器
变量处理器是对变量的类型化引用,由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()方法执行的操作相同。
对 Java 平台的这一更改旨在标准化调用以下类的方法的方式:
java.util.concurrent.atomicsun.misc.Unsafe
具体地说,是执行以下操作的方法:
- 访问/修改对象字段
- 数组的已访问/已修改元素
此外,这种变化导致了内存排序和对象可达性的两种栅栏操作。本着尽职尽责的精神,特别注意确保 JVM 的安全。确保这些更改不会导致内存错误非常重要。数据完整性、可用性,当然还有性能是上述尽职调查的关键组成部分,解释如下:
-
安全:不能出现损坏的内存状态。
-
数据完整性:必须确保对对象字段的访问使用相同的规则:
-
getfield字节码 -
putfield字节码 -
可用性:可用性的基准是
sun.misc.UnsafeAPI。目标是使新的 API 比基准更易于使用。 -
性能:与
sun.misc.UnsafeAPI 相比,性能无下降。目标是超越 API。
在 Java 中,栅栏操作是 Javac 以屏障指令的形式强制内存约束的操作。这些操作发生在屏障指令之前和之后,本质上是将它们封闭起来。
使用原子工具包
java.util.concurrent.atomic包是 12 个子类的集合,它们支持对线程安全和无锁的单个变量的操作。在此上下文中,线程安全是指访问或修改共享单个变量而不妨碍其他线程同时对该变量执行的代码。这个超类是在 Java7 中引入的。
下面是原子工具箱中 12 个子类的列表。如您所料,类名是自描述性的:
java.util.concurrent.atomic.AtomicBooleanjava.util.concurrent.atomic.AtomicIntegerjava.util.concurrent.atomic.AtomicIntegerArrayjava.util.concurrent.atomic.AtomicIntegerFieldUpdater<T>java.util.concurrent.atomic.AtomicLongjava.util.concurrent.atomic.AtomicLongArrayjava.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>
使用 AtoMIC 工具箱的关键是理解可变变量。可变变量、字段和数组元素可以由并发线程异步修改。
在 Java 中,volatile关键字用于通知 Javac 工具从主存中读取值、字段或数组元素,而不是缓存它们。
下面是一段代码片段,演示了对实例变量使用volatile关键字:
public class Sample {
private static volatile Sample myVolatileVariable; // a volatile
//
instance
//variable
// getter method
public static Sample getVariable() {
if (myVolatileVariable != null) {
return myVolatileVariable;
}
// this section executes if myVolatileVariable == null
synchronized(Sample.class) {
if (myVolatileVariable == null) {
myVolatileVariable = new Sample();
}
}
return null;
}
}
使用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 |
在 Java9 中,sun.misc.Unsafe类被指定要删除。实际上,编程行业对这一决定有一些反对意见。为了平息他们的担忧,这个阶级已经被贬低了,但不会被完全消除。
import语句废弃警告
通常,当我们编译程序时,会收到许多警告和错误。编译器错误必须被修复,因为它们在本质上是典型的语法错误。另一方面,应该对警告进行审查并适当处理。开发人员忽略了一些警告消息。
Java9 略微减少了我们收到的警告数量。特别是,不再生成由导入报表引起的废弃警告。在 Java9 之前,我们可以使用以下注解抑制不推荐使用的警告消息:
@SupressWarnings
现在,如果以下一种或多种情况为真,编译器将抑制废弃警告:
- 如果使用
@Deprecated注解 - 如果使用
@SuppressWarnings注解 - 如果警告生成代码和声明在祖先类中使用
- 如果警告生成代码在
import语句中使用
Coin 项目
Coin 项目是 Java7 中引入的一组小改动的特性集。这些变化如下:
switch语句中的字符串- 二进制整数字面值
- 在数字文本中使用下划线
- 实现多重捕获
- 允许更精确地重新触发异常
- 泛型实例创建的改进
- 带资源的
try语句的添加 - 调用
varargs方法的改进
对于 Java9 版本,Coin 项目有五个改进。这些增强功能将在下面的部分中详细介绍。
使用@SafeVarargs注解
从 Java9 开始,我们可以将@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语句
带资源的try语句以前要求在使用final变量时为语句中的每个资源声明一个新变量。以下是 Java9 之前的带资源的try语句的语法(在 Java7 或 Java8 中):
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.");
}
自 Java9 以来,带资源的try语句可以管理final变量,而不需要新的变量声明。因此,我们现在可以重写 Java9、10 或 11 中的早期代码,如图所示:
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语句块中,它提供了自动资源管理。一旦退出带资源的try语句块,资源将自动关闭。
您也可以使用finally块作为带资源的try语句的一部分。
使用菱形运算符
Java9 中引入了菱形操作符,如果推断的数据类型是可表示的,那么菱形操作符可以用于匿名类。当推断出数据类型时,它表明 Java 编译器可以确定方法调用中的数据类型。这包括声明和其中包含的任何参数。
菱形运算符是小于和大于符号对(<>),它对 Java9 并不陌生,相反,匿名类的具体用法是。
菱形操作符是在 Java7 中引入的,它简化了泛型类的实例化。以下是 Java7 之前的一个示例:
ArrayList<Student> roster = new ArrayList<Student>();
然后,在 Java7 中,我们可以重写它:
ArrayList<Student> roster = new ArrayList<>();
问题是这个方法不能用于匿名类。下面是 Java8 中一个运行良好的示例:
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
}
};
该错误是由于对匿名内部类使用菱形运算符而导致的。Java9 救命!虽然前面的代码在 Java8 中会导致编译时错误,但在 Java9、10 和 11 中工作正常。
停止使用下划线
下划线字符(_)不能再用作合法的标识符名称。先前删除标识符名称中下划线的尝试是不完整的。使用下划线将产生错误和警告的组合。自 Java9 以来,警告现在是错误。考虑以下示例代码:
public class UnderscoreTest {
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.");
}
}
}
在 Java8 中,前面的代码将导致针对int _ = 319;和if ( _ > 300语句的编译器警告。警告是:As of Java9, '_' is a keyword, and may not be used as an identifier。因此,在 Java9、10 或 11 中,不能单独使用下划线作为合法标识符。
使用非自描述性的标识符名称被认为是不好的编程实践。因此,将下划线字符本身用作标识符名称不应该是一个有问题的更改。
使用私有接口方法
Lambda 表达式是 Java8 版本的重要组成部分。作为这一改进的后续,接口中的私有方法现在是可行的。以前,我们不能在接口的非抽象方法之间共享数据。使用 Java9、10 和 11,这种数据共享是可能的。接口方法现在可以是私有的。让我们看一些示例代码
第一个代码片段是如何在 Java8 中编写接口的:
. . .
public interface characterTravel {
public 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;
}
}
从 Java9 开始,我们可以重写这段代码。正如您在下面的代码片段中所看到的,冗余代码已经被移动到一个名为characterTravel的私有方法中:
. . .
public interface characterTravel {
public 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
{
//
}
}
}
import语句处理
JDK 增强建议(JEP)216 是针对 Javac 如何处理导入语句而发布的。在 Java9 之前,如果源代码是否被接受,导入语句的顺序会产生影响
当我们用 Java 开发应用时,我们通常会根据需要添加import语句,从而导致import语句的无序列表。IDE 在对未使用的导入语句进行颜色编码方面做得很好,还可以通知我们需要的导入语句,但这还没有包括在内。导入语句的顺序应该无关紧要;没有适用的层次结构
javac编译类有两个主要步骤。具体到导入语句的处理,步骤如下:
- 类型解析:类型解析包括对抽象语法树的检查,以识别类和接口的声明
- 成员解析:成员解析包括确定类的层次结构、单个类变量和成员
从 Java9 开始,我们在类和文件中列出import语句的顺序将不再影响编译过程。让我们看一个例子:
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存在
下一步是成员解析,这就是 Java9 之前存在的问题所在。下面是 Javac 将用于为我们的示例代码执行成员解析的连续步骤的概述:
SamplePackage.OuterPackage的解析开始。- 处理
SamplePackage.OuterPackage.Nested导入。 SamplePackage.Outer.Nested类的决议开始。- 内部接口是经过类型检查的,但由于此时它不在范围内,因此无法解析内部接口。
SamplePackage.Thing的解析开始。此步骤包括将SamplePackage.Thing的所有成员类型导入范围。
因此,在我们的示例中,出现错误是因为在尝试解析时,Inner超出了范围。如果把第 4 步和第 5 步互换,就不会有问题了。
这个问题的解决方案是在 Java9 中实现的,它将成员解析步骤分解为额外的子步骤。以下是这些步骤:
- 分析导入语句
- 创建层次结构(类和接口)
- 分析类头部和类型参数
推断局部变量
从 Java10 开始,局部变量的声明已经简化。开发人员不再需要包含局部变量类型的清单声明;相反,可以通过使用新的var标识符来推断声明
使用var标识符推断声明
我们可以使用新的var标识符来推断数据类型,如下例所示。因此,我们不必显式声明数据类型,而是可以推断它们:
var myList = new ArrayList<String>();
前面的代码推断出了ArrayList<String>,因此我们不再需要使用详细的ArrayList<String> myList = new ArrayList<String>();语法。
引入var标识符不应被解释为向 Java 语言中添加new关键字。var标识符在技术上是一个保留的类型名
使用new标识符有一些限制。例如,当存在以下任何一种情况时,不能使用它们:
- 未使用初始化器
- 声明多个变量
- 使用数组维度括号
- 使用对初始化变量的引用
如预期的那样,如果var使用不正确,javac将发出特定的错误消息。
Lambda 参数的局部变量语法
正如本章前面所讨论的,var标识符是在 Java10 中引入的。在最新版本 Java11 中,var可以用在隐式类型的 Lambda 表达式中。以下是两个等效 Java 语句的示例:
(object1, object2) -> object1.myMyethod(object2)(var object1, var object2) -> object1.myMethod(object2)
在第一个语句中,不使用var标识符。在第二个语句中,使用了var。需要注意的是,如果在隐式类型的 Lambda 表达式中使用了var,则必须将其用于所有形式参数。
线程本地握手
版本 10 中添加到 Java 平台的一个特性是能够单独停止线程,而不必执行全局虚拟机安全点。拥有此功能的好处包括有偏差的锁撤销改进、虚拟机延迟减少、更安全的栈跟踪以及省略内存障碍。
这种变化在 x64 和 SPARC 系统中非常明显。如果我们想选择正常的安全点,我们将使用以下选项:
XX: ThreadLocalHandshakes
备用内存设备上的堆分配
从 Java10 开始,热点虚拟机支持非 DRAM 内存设备。我们可以使用以下选项在备用内存设备中分配 Java 对象堆:
XX:AllocateHeapAt=<file system path>
在使用备用设备文件系统分配内存时,解决位置冲突和安全问题非常重要。具体来说,请确保使用了正确的权限,并且在应用终止时清除堆。
根证书
从 Java10 的发布开始,JDK 中有一组默认的证书颁发机构(CA)证书。Java10 之前的 JDK 的cacerts和keystore不包含一组证书。在此 Java 版本之前,开发人员需要为cacerts和keystore创建和配置一组根证书
现在,Java 平台在cacerts和keystore中包含了一组由 Oracle 颁发的根证书。特定 CA 是 JavaSE 根 CA 程序的一部分。
从 Java10 开始,根证书中包括以下经 Oracle 验证的 CA:
- Actalis S.p.A.
- Buypass AS
- Camerfirma
- Certum
- Chunghwa Telecom Co., Ltd.
- Comodo CA Ltd.
- Digicert Inc
- DocuSign
- D-TRUST GmbH
- IdenTrust
- Let's Encrypt
- LuxTrust
- QuoVadis Ltd
- Secom Trust Systems
- SwissSign AG
- Tella
- Trustwave
很可能在 Java 平台的每个后续版本中都会添加额外的 ca
动态类文件常量
在 Java11 中,Java 类文件的文件格式被扩展为支持CONSTANT_Dynamic,它将创建委托给自举方法。Java 平台中增加了一个新的常量形式CONSTANT_Dynamic,它包含两个组件:
CONSTANT_InvokeDynamicCONSTANT_NameAndType
有关此功能增强的更多细节,请按照本章“进一步阅读”部分的链接找到。
删除 JavaEE 和 CORBA 模块
Java 企业版(JavaEE)和公共对象请求代理架构(CORBA)模块在 Java9 中被废弃,并从 Java11 开始从 Java 平台中移除。
以下 Java SE 模块包含 Java EE 和 CORBA 模块,已被删除:
- 聚合器模块(
java.se.ee) - 常用注解(
java.xml.ws.annotation - CORBA(
java.corba) - JAF(
java.activation) - JAX-WS(
java.xml.ws) - JAX-WS 工具(
jdk.xml.ws) - JAXB(
java.xml.bind) - JAXB 工具(
jdk.xml.bind) - JTA(
java.transaction)
总结
在本章中,我们介绍了 Java 平台的一些变化,这些变化会影响变量处理器、导入语句、对 Coin 项目的改进、局部变量类型推断、根证书、动态类文件常量等等。我们还介绍了废弃警告,以及在特定情况下为什么现在禁止这些警告。最后,我们探讨了导入语句处理的改进。
在下一章中,我们将研究 Jigsaw 项目指定的 Java 模块的结构。我们将深入探讨 Jigsaw 项目是如何作为 Java 平台的一部分实现的。本章使用代码片段演示 Java 的模块化系统,还将讨论 Java 平台在模块化系统方面的内部变化。
问题
- 什么是栅栏操作?
- 什么是 Coin 计划?
@SafeVarargs可以与什么类型的方法一起使用?import语句处理的变化有什么意义?- Java 在哪里存储根证书?
var不是关键字。这是怎么一回事?var是用来做什么的?- Java 平台的下划线字符(
_有什么变化? - 原子包中有多少子类?
- 哪个类管理变量处理器?
进一步阅读
下面列出的链接将帮助您深入了解本章介绍的概念:
四、用 Java11 构建模块化应用
在最后一章中,我们讨论了 Java 平台在变量处理器方面的最新变化,以及它们与原子工具包的关系。我们还讨论了贬值警告,以及在特定情况下抑制贬值警告的原因。研究了与项目 Coin 相关的变化,以及导入语句处理、推断局部变量和线程局部握手。我们通过查看堆分配、根证书、动态类文件常量以及 JavaEE 和 CORBA 模块的删除,进一步探讨了 Java 语言的变化。
在本章中,我们将研究由 projectjigsaw 指定的 Java 模块的结构,深入探讨如何将 Jigsaw 项目作为 Java 平台的一部分来实现。我们还将回顾 Java 平台与模块化系统相关的关键内部更改。
我们将研究以下主题:
- 模块化入门
- 模块化 JDK
- 模块化运行时映像
- 模块系统
- 模块化 Java 应用打包
- Java 链接器
- 封装大多数内部 API
技术要求
本章及后续章节主要介绍 Java11,Java 平台的标准版(SE)可从 Oracle 官方下载网站下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
模块化入门
我们可以将术语模块化定义为计算机软件的一种设计或构造。这种类型的软件设计涉及一组模块,这些模块共同构成了整个系统。例如,房子可以作为一个单一的结构或以模块化的方式建造,其中每个房间都是独立建造的,并连接起来形成一个家。通过这个类比,您可以有选择地添加模块或不添加模块来创建您的家庭。
模块的集合,在我们的类比中,成为你家的设计。您的设计不需要使用每个模块,只需要使用您想要的模块。因此,例如,如果有地下室和奖励房间模块,而您的设计不包括这些模块化房间,则这些模块不用于构建您的家。另一种选择是,每个家庭都包括每个房间,而不仅仅是使用的房间。这当然是浪费。让我们看看这和软件有什么关系。
这个概念可以应用于计算机架构和软件系统。我们的系统可以由几个组件组成,而不是一个庞然大物系统。正如您可能想象的那样,这为我们提供了一些特定的好处:
- 我们应该能够扩展 Java 应用以在小型设备上运行
- 我们的 Java 应用将更小
- 我们的模块化代码可以更有针对性
- 更多地使用面向对象编程模型
- 还有其他封装的机会
- 我们的代码将更高效
- Java 应用将提高性能
- 降低了整个系统的复杂性
- 测试和调试更容易
- 代码维护更容易
Java 向模块化系统的转变是必要的,原因有几个。以下是 Java9 之前的 Java 平台导致在当前 Java 平台中创建模块化系统的主要条件:
- Java 开发工具包(JDK)实在太大了。这使得很难支持小型设备。即使在下一节讨论的紧凑配置文件中,支持一些小型设备充其量也是困难的,在某些情况下是不可能的
- 由于 JDK 过大,我们的 Java 应用很难支持真正优化的性能。在这种情况下,越小越好
- Java 运行时环境(JRE)太大,无法有效地测试和维护我们的 Java 应用。这将导致耗时、低效的测试和维护操作
- Java 存档(JAR)文件也太大。这使得支持小型设备成了问题
- 由于 JDK 和 JRE 都是包罗万象的,所以安全性非常令人担忧,例如,Java 应用未使用的内部 API,由于公共访问修饰符的性质,仍然可用
- 最后,我们的 Java 应用太大了。
模块化系统具有以下要求:
- 必须有一个公共接口,以允许所有连接模块之间的互操作性
- 必须支持隔离和连接测试
- 编译时操作必须能够识别正在使用的模块
- 必须有对模块的运行时支持
模块概念最初是在 Java9 中引入的;它是一个命名的数据和代码集合。具体而言,Java 模块是以下内容的集合:
- 包
- 类
- 接口
- 代码
- 数据
- 资源
成功实现的关键在于,模块在其模块化声明中是自我描述的。模块名必须是唯一的,并且通常使用反向域名架构。下面是一个示例声明:
module com.three19.irisScan { }
模块声明包含在module-info.java文件中,该文件应位于模块的root文件夹中。正如人们所料,这个文件被编译成一个module-info.class文件,并将被放在适当的输出目录中。这些输出目录是在模块源代码中建立的
在下一节中,我们将研究 Java 平台在模块化方面的具体变化。
模块化 JDK
JEP-200 的核心目标是使用 Java 平台模块系统(JPMS)对 JDK 进行模块化。在 Java9 之前,我们对 JDK 的熟悉包括对其主要组件的了解:
- JRE
- 解释器(Java)
- 编译器(Javac)
- 归档器(Jar)
- 文档生成器(Javadoc)
模块化 JDK 的任务是将其分解为可在编译时或运行时组合的组件。模块化结构基于以下在 Java8 中作为紧凑概要文件建立的模块概要文件。下表详细介绍了这三种配置文件:
紧凑配置文件 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.tuil.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.drc |
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 |
java.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.naing.ldap |
javax.naming.spi |
javax.security.auth.kerberos |
javax.security.sasl |
javax.sql.rowset |
javax.sql.rowset.serial |
javax.sql.rowset.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 平台中标准化模块化系统的基础。标准化的有效性取决于以下六个原则:
- 所有 JCP 管理的模块都必须以字符串
java开头。因此,如果正在开发一个关于空间工具的模块,它的名称应该是java.spatial.util。
JCP 是指 Java 社区流程。 JCP 允许开发人员为 Java 创建技术规范。 您可以在 JCP 官方网站上了解有关 JCP 的更多信息并成为会员
- 非 JCP 模块被认为是 JDK 的一部分,它们的名称必须以字符串
jdk开头。 - 确保方法调用链接正常工作。下面的流程图最好地说明了这一点:

正如您在前面的流程图中所看到的,它只适用于导出包的模块。
- 第四个原则处理标准模块中使用的标准和非标准 API 包。以下流程图说明了本原则契约的实现情况:

- 第五个设计原则是标准模块可以依赖于多个非标准模块。虽然允许这种依赖关系,但不允许对非标准模块进行隐含的可读性访问
- 最终的设计原则确保非标准模块不会导出标准 API 包。
模块化源代码
如前所述,Jigsaw 项目的目标是模块化。设想的标准模块化系统将应用于 JavaSE 平台和 JDK。除了提高效率外,模块化转换还将带来更好的安全性和易维护性。JEP-201 中详细介绍的增强集中在 JDK 源代码重组上。让我们仔细看看。
重新组织 JDK 的源代码是一项重要的任务,并通过以下目标子集完成:
- 向 JDK 开发人员提供洞察和熟悉新的 Java9 模块化系统的信息。所以,这个目标是针对 JDK 的开发人员,而不是主流开发人员
- 确保在整个 JDK 构建过程中建立和维护模块化边界
- 第三个目标是确保未来的增强,特别是 Jigsaw 项目,能够轻松地集成到新的模块化系统中。
这种源代码重组的重要性怎么强调都不为过。Java9 之前的源代码组织已经有 20 年的历史了。这种过期的 JDK 源代码重组将使代码更易于维护。让我们看一下 JDK 源代码之前的组织结构,然后检查更改。
模块化前的 JDK 源代码组织
JDK 是代码文件、工具、库等的汇编。下图概述了 JDK 组件:

前面的图中 JDK 组件的预模块化组织将在下面的七个小节中详细介绍。
开发工具
开发工具位于\bin目录中。这些工具包括七个大的分类,每一个都将在后面的章节中详细介绍。
部署
这是一组用于帮助部署 Java 应用的工具:
appletviewer:该工具使您能够运行和调试 Java 小程序,而无需使用 Web 浏览器。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 拉丁 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:这是一个堆转储工具。它实例化了一个 Web 服务器,以便可以用浏览器查看堆jmap:显示进程、文件或服务器的堆和共享对象内存映射jsadebugd:这是 Java 的可服务性代理调试守护进程。它充当进程或文件的调试服务器jstack:这是一个 Java 栈跟踪工具,为进程、文件或服务器提供线程栈跟踪
Web 服务
这组工具提供了一个实用工具,可与 Java Web Start 和其他 Web 服务一起使用:
javaws:这是一个启动 JavaWebStart 的命令行工具。schemagen:该工具为 Java 架构生成模式。这些模式用于 XML 绑定。wsgen:该工具用于生成可移植的 JAX-WS 工件。wsimport:这个工具用于导入可移植的 JAX-WS 工件。xjc:这是用于 XML 绑定的绑定编译器。
JavaFX 工具
JavaFX 工具位于几个不同的地方,包括\bin、\man和\lib目录。
Java 运行时环境
JRE 位于\jre目录中。主要内容包括 JVM 和类库。
源代码
JDK 的源代码是 Java9 之前的版本,具有以下基本组织架构:
source code / [shared, OS-specific] / [classes / native] / Java API package name / [.file extension]
我们再仔细看看。在源代码之后,我们有两个选择。如果代码是跨平台的,那么它是一个共享目录;否则,它是特定于操作系统的。例如:
src/share/...
src/windows/...
接下来,我们有classes目录或本地语言目录。例如:
src/share/classes/...
src/share/classes/java/...
接下来,我们有 JavaAPI 包的名称,后跟文件扩展名。文件扩展名依赖于.java、.c等内容。
库
\lib目录包含\bin目录中一个或多个开发工具所需的类库。以下是典型 Java8\lib目录中的文件列表:

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

C 头文件
/include目录包含 C 头文件。这些文件主要支持以下内容:
- Java 本机接口(JNI):用于本机代码编程支持,JNI 用于将 Java 本机方法和 JVM 嵌入到本机应用中。
- JVM 工具接口(JVM TI):用于对运行 JVM 的应用进行状态检查和执行控制的工具。
数据库
ApacheDerby 关系数据库存储在/db目录中。您可以在以下站点了解有关 Java DB 的更多信息:
JDK 源代码重组
在上一节中,您了解到 Java9 之前的源代码组织模式如下:
source code / [shared, OS-specific] / [classes / native] / Java API package name / [.file extension]
在当前的 Java 平台中,我们有一个模块化的模式。该模式如下:
source code / module / [shared, OS-specific] / [classes / native / configuration] / [ package / include / library ] / [.file extension]
新模式中有一些不同之处,最明显的是模块名。在共享或 OS 特定目录之后,有类目录、用于 C 或 C++ 源文件的本机目录或配置目录。这种看似基本的组织模式更改会导致更易于维护的代码库。
模块化运行时映像
Java9 中引入的 Java 模块化系统需要更改运行时映像以实现兼容性。这些更改的好处包括以下方面的增强:
- 维修性
- 性能
- 安全
这些更改的核心是用于资源命名的新 URI 模式。这些资源包括模块和类。
统一资源标识符(URI)与统一资源定位器(URL)相似,它标识某物的名称和位置。对于 URL,某物是网页;对于 URI,它是资源。
**JEP-220 有五个主要目标,这些目标将在下面的章节中详细介绍。
采用运行时格式
为 Java9 创建了一个运行时格式,以供存储类和其他资源文件采用。此格式适用于以下情况下存储的类和资源:
- 当新的运行时格式比 Java9Jar 之前的格式具有更高的效率(时间和空间)时。
JAR 文件是 Java 归档文件。这是一种基于传统 ZIP 格式的压缩文件格式。
- 当存储的类和其他资源可以单独隔离和加载时。
- 当 JDK 和库类和资源可以存储时。这也包括应用模块。
- 当它们被设计成促进未来增强的方式时。这要求它们具有可扩展性、文档化和灵活性。
运行时映像重构
Java 中有两种类型的运行时映像:JDK 和 JRE。从 Java9 开始,这两种图像类型都被重新构造,以区分用户可以使用和修改的文件和开发人员及其应用可以使用但不能修改的内部文件。
在 Java9 之前的 JDK 构建系统同时生成了 JRE 和 JDK。JRE 是 Java 平台的完整实现。JDK 包括 JRE 以及其他工具和库。Java9 中一个显著的变化是 JRE 子目录不再是 JDK 映像的一部分。进行此更改的部分原因是为了确保两种图像类型(JDK 和 JRE)具有相同的图像结构。有了共同和重组的结构,未来的变革将更有效地结合起来。
如果您在 Java9 之前创建了针对特定结构的自定义插件,那么您的应用可能无法在 Java9 中工作。如果您显式地寻址tools.jar,这也是正确的。
下图提供了 Java9 发布前每个图像内容的高级视图:

Java9 运行时映像如下图所示。如图所示,完整的 JDK 映像包含与模块化运行时映像相同的目录以及demo、sample、man,并且包括目录:

JRE 和 JDK 映像之间不再有区别。在当前的 Java 平台上,JDK 映像是一个 JRE 映像,它包含一整套开发工具。
支持常见操作
开发人员有时必须编写代码来执行需要访问运行时映像的操作。Java9 包括对这些常见操作的支持。由于 JDK 和 JRE 运行时映像结构的重新构造和标准化,这是可能的。
剥夺 JDK 类的权限
当前的 Java 平台允许对单个 JDK 类进行特权撤销。此更改增强了系统安全性,因为它确保 JDK 类只接收系统操作所需的权限。
保留现有行为
JEP-220 的最终目标是确保现有的类不会受到负面影响。这是指不依赖于内部 JDK 或 JRE 运行时映像的应用。
模块系统
您会记得,创建模块化系统是为了为 Java 程序提供可靠的配置和强大的封装。这个实现的关键是链接时间的概念。如这里所示,链接时间是编译时和运行时之间的一个可选阶段。此阶段允许将适当的模块组装到优化的运行时映像中。
这在一定程度上是由于 JLink 链接工具的缘故,您将在本章后面详细了解该工具:

模块路径
重要的是要组织模块,以便它们可以很容易地定位。模块路径(模块组件或目录的序列)提供了搜索所使用的组织结构。依次搜索这些路径组件,返回包含模块的第一个路径组件。
模块及其路径不应被视为与包或类路径相同。他们确实是不同的,有更高水平的忠诚。关键的区别在于,对于类路径,将搜索单个组件。模块路径搜索返回完整的模块。这种类型的搜索可以通过按显示顺序搜索以下路径,直到返回模块:
- 编译模块路径
- 升级模块路径
- 系统模块路径
- 应用模块路径
让我们简要回顾一下这些路径。编译模块路径仅在编译时适用,并且包含模块定义。升级模块路径具有已编译的模块定义。系统模块是内置的,包括 JavaSE 和 JDK 模块。最后一个路径,即应用模块路径,包含来自应用模块和库模块的已编译模块定义。
访问控制边界冲突
作为一个专业的开发人员,您总是希望您的代码是安全的、可移植的和无 bug 的,这需要严格遵守 Java 构造,比如封装。在某些情况下,比如白盒测试,您需要打破 JVM 要求的封装。此授权允许跨模块访问。
为了允许破坏封装,您可以在模块声明中添加一个add-exports选项。以下是您将使用的语法:
module com.three19.irisScan
{
- - add-exports <source-module>/<package> = <target-module>
(, <target-module> )*
}
让我们仔细看看前面的语法。<source-module>和<targetmodule>是模块名,<package>是包名。使用add-exports选项允许我们违反访问控制边界。
关于使用add-exports选项有两条规则:
- 它可以在一个模块中多次使用
- 每次使用必须是
<source-module>和<targetmodule>的唯一配对
除非绝对必要,否则不建议使用add-exports选项。它的使用允许对库模块的内部 API 进行危险的访问。这种类型的使用使您的代码依赖于内部 API 而不会改变,这是您无法控制的。
运行时
热点虚拟机为jmod和jlink命令行工具实现<options>。
以下是jmod命令行工具的<options>列表:

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

模块化 Java 应用打包
Java9 以及 Java10 和 Java11 的最大改进之一是由 Java 打包器生成的运行时二进制文件的大小。这在一定程度上是由于 Java 链接器的缘故,这将在下一节中介绍。在当前的 Java11 中,JavaPackager 的工作流程基本上与 Java8 中的相同。您将在本节后面看到,工作流中添加了新工具。
Java 打包器只创建 JDK 应用。对 Java 打包器的这一更改旨在简化并提高生成运行时映像的过程的效率。因此,Java 打包器将只为与其关联的 SDK 版本创建运行时映像。
Java 链接器的高级研究
在 Java9 中引入 Java 链接器工具jlink之前,运行时映像创建包括复制整个 JRE。然后,移除未使用的组件。简单地说,jlink促进了只使用所需模块创建运行时映像。jlink被 Java 打包器用来生成嵌入式运行时映像。
Java 打包器选项
Java 打包器的语法如下:
javapackager -command [-options]
可以使用五种不同的命令(-command。具体描述如下:
| 命令 | 说明 |
|---|---|
-createbss |
此命令用于将文件从 CSS 转换为二进制文件。 |
-createjar |
这个命令与其他参数一起使用,创建一个 JAR 归档文件。 |
-deploy |
此命令用于生成 Java 网络启动协议(JNLP)和 HTML 文件。 |
-makeall |
此命令结合了-createjar、-deploy和编译步骤。 |
-signJar |
这个命令创建并签署一个 JAR 文件。 |
-createbss命令的[-options]包括:

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

-deploy命令的第一组[-options]包括:

-deploy命令的剩余[-options]集合包括以下内容:

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

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

Java 打包器分为两个模块:
jdk.packagerjdk.packager.services
Java 链接器
Java 链接器,通常称为 JLink,是一个创建自定义运行时映像的工具。该工具收集相应的模块及其依赖项,然后对它们进行优化以创建映像。这代表了 Java 的一个重大变化,它将在 Java9 的发行版中实现。在 Java 链接器工具 JLink 可用之前,运行时映像创建包括最初复制整个 JRE。在随后的步骤中,将删除未使用的组件。在当前的 Java 平台中,jlink只创建需要的模块的运行时映像。jlink由 Java 打包器生成嵌入式运行时映像。
如前一节所示,最近对 Java 平台的更改导致链接时成为编译时和运行时之间的可选阶段。正是在这个阶段,适当的模块被组装成一个优化的运行时映像。
JLink 是一个命令行链接工具,它允许创建包含较小 JDK 模块子集的运行时映像。这将导致更小的运行时映像。以下语法由四个组件组成jlink命令、选项、模块路径和输出路径:
$ jlink <options> ---module-path <modulepath> --output <path>
以下是可与jlink工具一起使用的选项列表,以及每个选项的简要说明:

模块路径告诉链接器在哪里可以找到模块。链接器不会使用分解的模块或 JAR/JMOD 文件。
输出路径只是通知链接器保存自定义运行时映像的位置。
封装大多数内部 API
JEP-260 的实现使 Java 平台更加安全。JEP 的核心目标是封装大多数内部 API。具体来说,JDK 的大多数内部 API 在默认情况下不再可访问。目前,被认为是关键和广泛使用的内部 API 仍然可以访问。在将来,我们很可能会看到替代它们的功能,届时,默认情况下,这些内部 API 将无法访问。
那么,为什么这种改变是必要的呢?有一些广泛使用的 API 是不稳定的,在某些情况下是不标准的。不受支持的 API 不应访问 JDK 的内部详细信息。因此,JEP-260 提高了 Java 平台的安全性,一般来说,您不应该在开发项目中使用不受支持的 API。
上述关键 API(JDK 内部)如下所示:
sun.miscsun.misc.Unsafesun.reflect.Reflectionsun.reflect.ReflectionFactory.newConstrutorForSerialization
上述关键的内部 API 在当前 Java 平台中仍然可以访问。它们可以通过jdk.unsupportedJDK 模块访问。完整的 JRE 和 JDK 映像将包含jdk.unsupported模块。
您可以使用 Java 依赖性分析工具jdeps来帮助确定 Java 程序是否依赖于 JDK 内部 API。
这是一个有趣的变化。在未来的 Java 版本中,默认情况下,当前可访问的内部 API 可能无法访问。
总结
在本章中,我们检查了 Jigsaw 项目指定的 Java 模块的结构,并深入了解了如何实现 Jigsaw 项目以改进 Java 平台。我们还回顾了 Java 平台与模块化系统相关的关键内部更改。我们的回顾从模块化入门开始,从好处和需求的角度了解了 Java 的模块化系统。
我们探讨了构成 JDK 的七个主要工具类别。正如我们所了解的,Java 中的模块化还扩展到运行时映像,从而提高了可维护性、性能和安全性。链接时间的概念是作为编译时和运行时之间的可选阶段引入的。在本章的结尾,我们介绍了 Java 链接器以及 Java 如何封装内部 API。
在下一章中,我们将探讨如何将现有的应用迁移到当前的 Java 平台。我们将研究手动和半自动迁移过程。
问题
- 导致 Java 平台模块化的主要因素是什么?
- 模块化系统的四个强制性要求是什么?
- Java 模块是哪六个组件的集合?
- 所有 JCP 管理的模块都以什么前缀开头?
- JDK 的七个主要组件是什么?
- 模块化运行时映像有什么好处?
- 模块化运行时映像中有哪些目录?
- 完整 JDK 映像中有哪些目录?
- 编译时和运行时之间的可选阶段是什么?
- Java 打包器创建的二进制文件比以前的 Java 版本小是什么原因?
进一步阅读
此处列出的参考资料将帮助您深入了解本章中介绍的概念:
五、将应用迁移到 Java11
在前一章中,我们仔细研究了项目 Jigsaw 指定的 Java 模块的结构,并研究了如何实现项目 Jigsaw 来改进 Java 平台。我们还回顾了 Java 平台的关键内部更改,并特别关注新的模块化系统。我们从一个模块化入门开始,在这里我们了解了 Java 的模块化系统的好处和需求。接下来,我们将探讨 JDK 的模块化,包括如何重新组织源代码。我们还研究了 JDK 的七个主要工具类别,并了解到 Java 模块化扩展到运行时映像,从而提高了可维护性、更好的性能和提高了安全性。引入链路时间的概念,作为编译时间与运行时之间的可选阶段。我们在结束这一章时,将介绍 Java 链接器以及 Java 如何封装内部 API。
在本章中,我们将探讨如何将现有的应用迁移到当前的 Java 平台。我们将研究手动和半自动迁移过程。本章旨在为您提供一些见解和过程,使您的非模块化 Java 代码能够在当前的 Java 平台上工作。
我们将在本章讨论的主题如下:
- Jigsaw 项目快速回顾
- 模块如何适应 Java 环境
- 迁移规划
- Oracle 的建议
- 部署
- 有用的工具
技术要求
本章及后续章节介绍 Java11。Java 平台的标准版(SE)可从 Oracle 官方下载网站下载。
一个集成开发环境(IDE)包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
本章的源代码可以在 GitHub 的 URL 上找到。
Jigsaw 项目快速回顾
Jigsaw 项目是一个 Java 项目,它包含了对 Java 平台的几个更改建议。正如您在前面几章中所读到的,Java9 中对 Java 平台最重要的更改涉及到模块和模块化。转移到 Java 模块的计划是由 Jigsaw 项目推动的。对模块化的需求源于 Java 的两大挑战:
- 类路径
- JDK 的整体性
接下来,我们将回顾这两个挑战,并了解如何使用当前的 Java 平台解决和克服它们。
类路径
在 Java9 之前,类路径是有问题的,也是开发人员痛苦的根源。这一点在众多的开发者论坛上表现得很明显,幸运的是,Oracle 对此给予了关注。下面是类路径可能有问题的几个实例;下面是两个主要的例子:
- 第一种情况涉及在开发计算机上有两个或多个版本的库。Java 系统以前处理这个问题的方式是不一致的。在类加载过程中使用了哪个库并不容易辨别。这导致了不希望的缺乏特异性,而没有足够的关于加载哪个库的细节
- 第二种情况是使用类加载器的最高级功能。通常情况下,这种类型的类加载器的使用会导致最多的错误和 bug。这些错误和 bug 并不总是很容易被发现,并且会给开发人员带来很多额外的工作。
在 Java9 之前,类路径几乎总是非常长的。在最近的一次演示中,Oracle 共享了一个包含 110 个 JAR 文件的类路径。这类笨拙的类路径很难检测到冲突,甚至很难确定是否缺少任何内容,如果缺少,可能缺少什么。将 Java 平台重新设想为一个模块化的系统使得这些类路径问题成为过去。
模块通过提供可靠的配置来解决 Java9 之前的类路径问题。
JDK 的整体性
自 1995 年以来,Java 以一种令人印象深刻的方式不断发展,随着每一步的发展,JDK 都变得越来越大。与 Java8 一样,JDK 已经变得非常庞大。在 Java9 之前,由于 JDK 的整体性,存在一些问题,包括以下问题:
- 因为 JDK 太大了,它不适合非常小的设备。在一些开发部门,这就足够找到解决软件工程问题的非 Java 解决方案了
- 过大的 JDK 导致了浪费。在设备、网络和云上运行时,它在处理和内存方面是浪费的。这源于这样一个事实:即使只需要 JDK 的一小部分,也会加载整个 JDK
- 虽然 Java 平台在运行时有很好的性能,但是从负载和启动时间来看,启动性能还有很多需要改进的地方
- 大量的内部 API 也是一个难点。因为有太多的内部 API 存在并且被开发人员使用,所以系统很难进化
- 内部 API 的存在使得 JDK 很难实现安全性和可伸缩性。由于存在如此多的内部依赖关系,隔离安全性和可伸缩性问题是非常困难的。
解决 JDK 整体问题的答案是模块。Java9 引入了该模块及其自己的模块化系统。对平台的一个重大更新是只编译所需的模块,而不是编译整个 JDK。这一模块化系统涵盖了整个这本书。
模块通过提供强大的封装解决了 Java9JDK 之前的单片问题。
模块如何适应 Java 环境
如下图所示,包由类和接口组成,模块由包组成。模块是包的容器。这是 Java 模块化系统的基本前提,在一个非常高的层次上。重要的是将模块视为模块化系统的一部分,而不是简单地将其视为包之上的新抽象级别,如下图所示:

所以,模块是 Java9 的新成员,正如您所料,它们需要声明才能使用。一个模块的声明包括它具有依赖关系的其他模块的名称。它还为其他依赖于它的模块导出包,模块化声明可以说是开始使用 Java 开发时需要解决的最重要的模块化问题。举个例子:
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 应用时,模块声明将被放置在module-info.java文件中。一旦这个文件完成,您只需运行 Java 编译器 Javac,生成module-info.classJava 类文件。您完成此任务的方式与当前将.java文件编译为.class文件的方式相同
您还可以创建模块化 JAR 文件,将您的module-info.class文件放在其根目录下,这代表了极大的灵活性。
接下来,让我们回顾一下有关 Java 模块的三个重要概念:
- 基本模块
- 可靠的配置
- 强封装
基本模块
Java 模块概念的核心是理解基本模块。在编程 Java 应用或移植使用旧版本 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 平台创建应用时,就可以知道哪些是可用的。
您会注意到,在上一节中,我们没有在com.three19.irisScan模块的声明中包含所需的java.base:代码行。更新后的代码如下所示,现在包括所需的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;
}
如果您没有在模块声明中包含所需的代码行java.base,Java 编译器将自动包含它。
可靠的配置
正如本章前面提到的,模块为我们的 Java 应用提供了可靠的配置,解决了 Java 平台早期版本中的类路径问题。
Java 读取和解释模块声明,使模块可读。这些可读模块允许 Java 平台确定是否有任何模块丢失,是否声明了重复的库,或者是否存在任何其他冲突。在 Java 版本 9、10 和 11 中,编译器或运行时将生成和输出非常特定的错误消息。以下是编译时错误的示例:
src/com.three19.irisScan/module-info.java: error: module not found:
com.three19.irisScan
requires com.three19.irisCore;
下面是一个运行时错误的例子,如果没有找到模块com.three19.isrisCore,但是com.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 的强封装解决了整体 JDK 问题。
封装是 OOP 的核心概念,它保护对象不受外部代码的影响。强封装的特性是指封装的良好编程实现。
在 Java 中,封装是由module-info.java文件中的信息驱动的。这个文件中的信息让 Java 知道哪些模块依赖于其他模块,以及每个模块输出什么。这强调了确保我们的moduleinfo-java文件正确配置的重要性。在模块化之前,让我们看一个用标准 Java 代码编写的示例:

在前面的例子中,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();
如果编译器没有捕捉到此错误(可能性很小),则会发生以下运行时错误:
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.
详细的错误消息将使调试和故障排除更加容易。
迁移规划
如果您正在维护使用 Java8 或更早版本构建的 Java 应用,则应该考虑将应用更新到现代 Java 平台。由于 PostJava8 平台与早期版本有很大不同,因此迁移应用时需要有目的的方法。提前计划,考虑最可能遇到的问题是谨慎的。在我们研究这些问题之前,让我们在下一节测试一个简单的 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("aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrR
sStTuUvVwWxXyYzZ0123456789+-*/?!@#$%");
}
return newPassword;
}
public static String randomizeFromSet(String characterSet) {
int len = characterSet.length();
int ran = (int)(len * Math.random());
return characterSet.substring(ran, ran + 1);
}
}
在下面的屏幕截图中,我们在运行 Java8 的 Mac 上测试了GeneratePassword应用。如您所见,我们首先查询 Java 以验证当前版本。在这个测试中,使用了 Java1.8.0_121。接下来,我们使用javac工具编译GeneratePasswordJava 文件。最后,我们运行应用:

从前面的测试中可以看到,GeneratePassword.java被成功编译,生成了GeneratePassword.class文件。应用是使用java GeneratePassword命令运行的。提示用户输入所需的密码长度,并输入了32。然后,应用成功地生成了一个 32 个字符的随机密码,并提供了相应的输出
这个测试证明了这个示例应用使用 JDK1.8 可以成功地工作。接下来,让我们使用 JDK10 测试相同的应用:

如您所见,我们清楚地演示了 Java9 之前的应用有可能在 Java10 上成功运行,而无需进行任何修改。这是一个简单的案例研究,具有一个非常基本的 Java 程序。当然,这是最好的情况,不能假设。您需要测试应用,以确保它们在当前 Java 平台上按预期运行。
在下一节中,我们将回顾在使用新的 Java 平台测试 Java9 之前的应用时可能遇到的一些潜在问题。
潜在的迁移问题
本节介绍的潜在迁移问题包括直接访问 JRE、访问内部 API、访问内部 JAR、jarURL 废弃、扩展机制和 JDK 的模块化。让我们看看每一个潜在的迁移问题。
JRE
创建 Java 的模块化系统使得开发工具和实用工具的数量和位置得到了一些简化。一个这样的例子是 JDK 对 JRE 的使用。在所有 Java9 之前的版本中,Java 平台都将 JDK 和 JRE 作为两个独立的组件包含在内。从 Java9 开始,这些组件已经组合在一起。这是一个重要的变化,开发人员应该非常清楚。如果您有一个专门指向 JRE 目录的应用,则需要进行更改以避免出现问题。JRE 内容如下:

访问内部 API
当前的 Java 平台封装了内部 API,以提高用 Java 编写的平台和应用的安全性。与以前版本的 Java 平台不同,用 Java9、10 或 11 编写的应用将不具有对 JDK 内部 API 的默认访问权限。Oracle 已经确定一些内部 API 是关键的,这些 API 仍然可以通过 JDK 模块访问。
上述关键 API(JDK 内部)如下所示:
sun.miscsun.misc.Unsafesun.reflect.Reflectionsun.reflect.ReflectionFactory.newConstrutorForSerialization
如果您有实现任何sun.*或com.sun.*包的 pre-Java9 应用,那么将应用迁移到当前 Java 平台时可能会遇到问题。为了解决这个问题,您应该检查您的类文件以使用sun.*和com.sun.*包。或者,您可以使用 Java 依赖性分析工具jdeps来帮助确定您的 Java 程序是否对 JDK 内部 API 有任何依赖性。
jdeps工具是 Java 依赖性分析工具;它可以用来帮助确定 Java 程序是否对 JDK 内部 API 有任何依赖性。
访问内部 Jar
从版本 9 开始,Java 不允许访问内部 Jar,如lib/ant-javax.jar、lib/dt.jar和lib目录中列出的其他 Jar,如下所示:

这里需要注意的关键是,如果您的 Java 应用依赖于lib文件夹中的这些工具之一,那么您需要相应地修改代码。
建议您在开始使用 Java10 和 Java11 之后测试 IDE,以确保 IDE 得到更新并正式支持最新版本的 Java。如果您使用多个 IDE 进行 Java 开发,请测试每一个 IDE 以避免意外。
JAR URL 废弃
JAR 文件 URL 在 Java9 之前被一些 API 用来标识运行时映像中的特定文件。这些 URL 包含一个带有两条路径的jar:file:前缀,一条路径指向jar,另一条路径指向jar中的特定资源文件。以下是 Java9 JAR 之前的 URL 的语法:
jar:file:<path-to-jar>!<path-to-file-in-jar>
随着 Java 模块化系统的出现,容器将容纳资源文件,而不是单独的 JAR。访问资源文件的新语法如下:
jrt:/<module-name>/<path-to-file-in-module>
一个新的 URL 模式jrt现在已经就位,用于命名运行时映像中的资源。这些资源包括类和模块。新的模式允许在不给运行时映像带来安全风险的情况下识别资源。这种增强的安全性确保运行时映像的形式和结构保持隐藏。新架构如下:
jrt:/[$MODULE[/$PATH]]
有趣的是,jrtURL 的结构决定了它的含义,这表明该结构可以采用几种形式之一。以下是三个不同的jrtURL 结构示例:
jrt:/$MODULE/$PATH:此结构提供对$MODULE参数指定模块内的$PATH参数标识的资源文件的访问jrt:/$MODULE:该结构可参照$MODULE参数指定模块内的所有资源文件jrt:/:此结构提供对运行时映像中所有资源文件的引用
如果您已经存在使用 API 返回的 URL 实例的代码,那么您应该不会有任何问题。另一方面,如果您的代码依赖于jarURL 结构,则会出现问题。
扩展机制
Java 平台以前有一个扩展机制,使开发人员能够为所有应用提供定制 API。如下图所示,扩展是 Java 平台的插件或附加组件。默认情况下,每个扩展中的 API 和类都自动可用:

如图所示,Java 应用可以访问 Java 平台和扩展,而不需要类路径。此功能在 Java8 中已被弃用,并且在当前版本的 Java 中不再存在。
JDK 的模块化
到目前为止,您已经对 Java 的模块化有了明确的认识。Java 和另一种面向对象编程语言中的一句老话是一切都是一个类。现在,一切都是一个模块是一句新的格言。有三种类型的模块,如下所述:
| 模块类型 | 说明 |
|---|---|
| 自动 | 当一个 JAR 被放置在一个新的模块路径上时,就会自动创建模块。 |
| 显式/命名 | 这些模块是通过编辑module-info.java文件手动定义的。 |
| 未命名 | 当 JAR 被放置在类路径上时,将创建未命名的模块。 |
从 8 或更早版本迁移应用时,应用及其库将成为未命名的模块。因此,您需要确保所有模块都在模块路径中
另一件需要注意的是,运行时映像不会包含整个 JDK,相反,它只包含应用所需的模块。值得回顾一下 JDK 是如何在 Java 中模块化的。下表包含当前 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 SE 的 API 规范:
java.activation |
java.base |
java.compiler |
java.corba |
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.sasl |
java.sql |
java.sql.rowset |
java.transaction |
java.xml |
java.xml.bind |
java.xml.crypto |
java.xml.ws |
java.xml.ws.annotation |
记住,默认情况下,所有应用都可以访问模块路径中的java.base。
下表包含 Java 中 JavaFX 的 API 规范:
javafx.base |
javafx.controls |
javafx.fxml |
javafx.graphics |
javafx.media |
javafx.swing |
javafx.web |
有两个附加模块:
java.jnlp定义 JNLP 的 API(简称 Java 网络启动协议)。java.smartcardio定义 Java 智能卡输入/输出的 API。
有关这些模块的详细信息,请访问 Oracle 的 Java® 平台,Java 标准版开发套件版本 10 API 规范网站。
Oracle 的建议
Oracle 在不断更新 Java 平台方面做得很好,他们对从旧版本迁移到新 JDK 的见解值得回顾。在本节中,我们将介绍准备步骤、打破封装、对运行时映像的更改、已删除的工具和 API 等组件、对垃圾收集的更改以及部署。
准备步骤
Oracle 提供了一个五步流程,帮助开发人员将 Java 应用从 Java9 以前的版本迁移到现代版本 9、10 或 11。以下列出了这些步骤,随后的部分将介绍这些步骤:
- 获取 JDK 早期访问构建
- 重新编译前运行程序
- 更新第三方库和工具
- 编译应用
- 在你的代码上运行
jdeps
获取 JDK 早期访问构建
如果您是在 Java11(18.9)正式发布之前阅读本书,那么您可以从以下链接获得 JDK11 早期访问构建。
早期版本可用于 Windows(32 和 64)、MacOS(64)、Linux(32 和 64)以及各种 Linux ARM、Solaris 和 Alpine Linux 版本。
在正式发布 Java11 之前花点时间测试 Java9 应用并迁移它们,这将有助于确保依赖于 Java 应用的服务不会出现任何停机。
您可以从以下链接下载版本 9 和 10:
重新编译前运行程序
如本章前面所述,您现有的 Java 应用有可能在 Java11 平台上运行而不进行修改。因此,在进行任何更改之前,请尝试在 Java9 平台上运行当前的应用。如果您的应用在 Java11 上运行得很好,那就太好了,但是您的工作还没有完成。回顾下面的三个部分:更新第三方库和工具、编译应用以及在代码上运行jdeps。
更新第三方库和工具
第三方库和工具可以帮助扩展我们的应用并缩短开发时间。对于 Java 兼容性,确保您使用的每个第三方库和工具都与 JDK 的最新版本兼容并支持它是很重要的。在 Java11 上运行应用并不能为您提供所需的洞察力级别,以确保您不会遇到兼容性问题。建议您查看每个库和工具的官方网站,以验证与 JDK18.9 的兼容性和支持。
如果您使用的库或工具的版本确实支持 JDK18.9,请下载并安装它。如果您发现一个还不支持 JDK18.9,请考虑找一个替代品。
在我们的上下文中,工具包括 IDE。NetBeans、Eclipse 和 IntelliJ 都有支持 JDK11 的 IDE 版本。这些网站的链接如下:
编译应用
下一步是使用 JDK 的javac编译应用。这一点很重要,即使您的应用可以很好地与最新的 JDK 配合使用。您可能不会收到编译器错误,但也要注意警告。以下是您的应用可能无法使用新 JDK 编译的最常见原因,假设它们在 Java9 之前编译良好。
首先,如本章前面所述,大多数 JDK 的内部 API 在默认情况下是不可访问的。您的指示将是运行时或编译时的IllegalAccessErrors错误。您需要更新代码,以便使用可访问的 API
Java9 之前的应用可能无法使用 JDK18.9 编译的第二个原因是,如果将下划线字符用作单个字符标识符。根据 Oracle 的说法,这种做法在 Java8 中生成警告,在 Java9、10 和 11 中生成错误。让我们看一个例子。下面的 Java 类实例化了一个名为_的Object,并向控制台输出一条单数消息:
public class Underscore {
public static void main(String[] args) {
Object _ = new Object();
System.out.println("This ran successfully.");
}
}
当我们用 Java8 编译这个程序时,我们收到一个警告,在 Java SE 8 之后的版本中可能不支持使用_作为标识符:

正如您在下面的屏幕截图中看到的,这只是一个警告,应用运行正常:

现在让我们尝试使用 JDK9 编译同一个类:

如您所见,使用下划线作为单个字符标识符仍然只会导致警告而不是错误。应用已成功运行。这个测试是在 JDK9 还处于早期版本时运行的。
在 Java10 和 Java11 中,使用_作为标识符是非法的。下面的屏幕截图显示了编译Underscore.java应用的尝试:

您的预 Java9 编程应用不使用 JDK9、10 或 11 编译的第三个潜在原因是您使用的是-source和-target编译器选项。让我们看一下 Java9 之前和 Java10 之后的-source和-target编译器选项。
Java9 之前的源和目标选项
-source选项指定 Java SE 版本,并具有以下可接受的值:
| 值 | 说明 |
|---|---|
| 1.3 | javac不支持 JavaSE1.3 之后引入的特性。 |
| 1.4 | javac接受具有 JavaSE1.4 中引入的语言特性的代码。 |
| 1.5 或 5 | javac接受具有 JavaSE1.5 中引入的语言特性的代码。 |
| 1.6 或 6 | javac将编码错误报告为错误而不是警告。值得注意的是,JavaSE1.6 没有引入新的语言特性。 |
| 1.7 或 7 | javac接受具有 JavaSE1.7 中引入的语言特性的代码。如果不使用-source选项,这是默认值。 |
-target选项告诉javac目标 JVM 的版本。-target选项的可接受值为:1.1、1.2、1.3、1.4、1.5或5、1.6或6和1.7或7。如果未使用-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 |
Java10 和 Java11 的源和目标选项
在 Java9 中,支持的值如下所示:
| 支持值 | 备注 |
|---|---|
| 11 | 当 JDK11 发布时,这很可能成为默认值。 |
| 10 | 从 JDK10 开始,这是默认值,不应指定任何值。 |
| 9 | 将支持设置为 1.9。 |
| 8 | 将“支持”设置为 1.8。 |
| 7 | 将支持设置为 1.7。 |
| 6 | 将 support 设置为 1.6,并生成一个警告(不是错误)来指示 JDK6 已废弃。 |
在代码上运行jdeps
jdeps类依赖性分析工具对 Java 来说并不新鲜,但对于开发人员来说,它可能从未像 Java 模块化系统的出现那样重要。将应用迁移到 Java9、10 或 11 的一个重要步骤是运行jdeps工具来确定应用及其库的依赖关系。如果您的代码依赖于任何内部 API,jdeps工具可以很好地建议替换。
以下屏幕截图显示了使用jdeps分析仪时可用的选项:

当您使用jdeps -help命令时,您还会看到模块相关的分析选项、过滤依赖项的选项和过滤要分析的类的选项。
让我们看一个例子。下面是一个名为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使用 Java8 编译这个类:

如您所见,Java8 成功地编译了类并运行了应用。编译器确实给了我们一个警告。现在让我们看看当我们尝试使用 Java9 编译这个类时会发生什么:

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

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

如您所见,我们有三个依赖项:java.io、java.lang和sun.misc。在这里,我们建议用rt.jar替换我们的sun.misc依赖关系。
作为最后的测试,我们将尝试使用 Java10 编译DependencyTest:

在这里,我们看到我们根本无法编译应用。JDK10 和 11 都提供了相同的错误。
破坏封装
当前的 Java 平台比以前的版本更安全,部分原因是模块化重组导致了封装的增加。也就是说,您可能需要突破模块化系统的封装。
正如您在本章前面所读到的,大多数内部 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非法访问要在类路径上实现的操作。由于这里的操作非常非法,每次发生这些操作时,您都会收到警告。
运行时映像更改
在 JDK 和 JRE 方面,当前的 Java 与 Java8 和更早的版本有很大的不同。这些变化大多与模块化有关,并已在其他章节中介绍。还有一些事情你应该考虑。
Java 版本模式
在 Java9 中,Java 平台版本的显示方式发生了变化。以下是 Java8 版本格式的示例:

现在让我们看看 Java9 是如何报告其版本的:

如您所见,对于 Java9,版本模式是$MAJOR.$MINOR.$SECURITY.$PATCH。这与以前的 Java 版本有明显的不同。只有当您有解析由java -version命令和选项返回的字符串的代码时,这才会影响您的应用。
最后,让我们看看 Java10(18.3)如何报告其版本:

对于 Java10、11,在可预见的将来,版本模式是$YY.$MM。这是从 Java10 开始的变化。如果您有任何代码来计算由java -version命令和选项返回的内容,则可能需要更新代码。
JDK 和 JRE 的布局
文件在 JDK 和 JRE 中的组织方式在 Java 的新版本中发生了变化。花时间熟悉新的文件系统布局是值得的。下面的屏幕截图显示了 JDK 的/bin文件夹的文件结构:

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

删除了什么?
Java 平台新版本的另一个变化是许多平台组件被删除。以下部分是最重要的组成部分。
值得注意的是,rt.jar和tools.jar以及dt.jar已经被移除。这些 JAR 文件包含类和其他资源文件,并且都位于/lib目录中。
已删除认可的标准覆盖机制。在 Java 中,如果检测到这个机制,javac和java都将退出。该机制用于应用服务器覆盖一些 JDK 组件。在 Java 中,可以使用可升级的模块来实现相同的结果。
如本章前面所述,扩展机制也已拆除。
以下列出的 API 以前已被废弃,已被删除,在当前 Java 平台中不可访问。删除这些 API 是 Java 平台模块化的结果:
apple.applescriptcom.apple.concurrentcom.sun.image.codec.jpegjava.awt.dnd.peerjava.awt.peerjava.rmi.server.disableHttpjava.util.logging.LogManager.addPropertyChangeListenerjava.util.logging.LogManager.removePropertyChangeListenerjava.util.jar.Pack200.Packer.addPropertyChangeListenerjava.util.jar.Pack200.Packer.removePropertyChangeListenerjava.util.jar.Pack200.Unpacker.addPropertyChangeListenerjava.util.jar.Pack200.Unpacker.removePropertyChangeListenerjavax.management.remote.rmi.RMIIIOPServerImplsun.misc.BASE64Encodersun.misc.BASE64Decodersun.rmi.transport.proxy.connectTimeoutsun.rmi.transport.proxy.eagerHttpFallbacksun.rmi.transport.proxy.logLevelsun.rmi.transport.tcp.proxy
下列列出的工具已被删除。在每种情况下,该工具以前都被贬低,或其功能被更好的替代品取代:
hprofjava-rmi.cgijava-rmi.exeJavaDBjhatnative2ascii
Java 中删除的另外两个内容如下:
- AppleScript 引擎。这台发动机被视为无法使用,未经更换就报废了。
- Windows 32 位客户端虚拟机。JDK9 确实支持 32 位服务器 JVM,但不支持 32 位客户端 VM。这一变化的重点是提高 64 位系统的性能。
更新的垃圾收集
垃圾收集一直是 Java 声名鹊起的原因之一。在 Java9 中,垃圾优先(G1)垃圾收集器现在是 32 位和 64 位服务器上的默认垃圾收集器。在 Java8 中,默认的垃圾收集器是并行垃圾收集器。Oracle 报告说,有三种垃圾收集组合将禁止您的应用在 Java9 中启动。这些组合如下:
- DefNew + CMS
- 增量 CMS
- ParNew + SerialOld
我们将在第 7 章“利用默认的 G1 垃圾收集器”中深入了解 Java9 垃圾收集。
部署应用
在部署应用时,从 Java8 或更早版本迁移到当前 Java 平台时,有三个问题需要注意。这些问题包括 JRE 版本选择、序列化小程序和 JNLP 更新。
JNLP 是 Java 网络启动协议的首字母缩写,本章后面的部分将对此进行介绍。
选择 JRE 版本
在 Java9、10 和 11 之前,开发人员可以在启动应用时请求 JRE 版本,而不是正在启动的版本。这可以通过命令行选项或正确的 JAR 文件清单配置来实现。由于我们通常部署应用的方式,JDK9 中已经删除了这个特性。以下是三种主要方法:
- 活动安装程序
- 使用 JNLP 的 Java Web Start
- 本机操作系统打包系统
序列化 Applet
Java 不再支持将 Applet 作为序列化对象进行部署。过去,Applet 被部署为序列化对象,以补偿压缩速度慢和 JVM 性能问题。在当前的 Java 平台上,压缩技术是先进的,JVM 具有良好的性能。
如果尝试将小程序部署为序列化对象,则在启动小程序时,对象属性和参数标记将被忽略。从 Java9 开始,您可以使用标准部署策略部署小程序。
JNLP 更新
JNLP 用于使用 Web 服务器上的资源在桌面客户端上启动应用。JNLP 客户端包括 JavaWebStart 和 Java 插件软件,因为它们能够启动远程托管的 Applet。该协议有助于启动 RIA。
RIAs(简称富互联网应用),当使用 JNLP 启动时,可以访问各种 JNLP API,在用户许可的情况下,可以访问用户的桌面。
JNLP 规范在 Java9 中进行了更新。以下章节详细介绍了四个具体更新。
嵌套资源
以前支持将组件扩展与 Java 或 J2SE 元素中的嵌套资源一起使用,但规范中没有对此进行说明。规范现在已经更新以反映这种支持。先前的规范如下:
不能将 Java 元素指定为资源的一部分。
更新后的规范内容如下:
组件扩展中的 Java 元素不会控制所使用的 Java 版本,但可以使用包含嵌套资源元素的 Java 版本,并且只有在使用与第 4.6 节中指定的给定版本匹配的 Java 版本时,才可以使用这些资源。
这个特定的更改确保扩展 JLP 文件必须具有 Java 或 J2SE 资源,并且这些资源不会指定使用什么 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 的更多信息,请参阅官方文档。
<application-desc>元素的第二个变化是增加了param子元素。这允许我们使用value属性提供参数的名称及其值。下面是 JNLP 文件的<application-desc元素在包含param子元素和value属性的情况下的外观示例。
此示例显示了三组参数:
<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 规范。在 Java9 之前,您可以使用&创建复杂的比较。标准 XML 不支持这一点。您仍然可以在 JNLP 文件中创建复杂的比较。现在您将使用&;而不是&。
数字版本比较
JNLP 规范已经更改,以反映数字版本元素与非数字版本元素的比较方式。在更改之前,版本元素是通过 ASCII 值按字典顺序进行比较的。在当前的 Java 平台和 JNLP 规范发生变化的情况下,元素仍然是按 ASCII 值按字典顺序进行比较的。当两个弦的长度不同时,这种变化就很明显了。在新的比较中,较短的字符串将填充前导零以匹配较长字符串的长度。
词典比较使用基于字母顺序的数学模型。
有用的工具
本节重点介绍三种工具,它们可以帮助您将应用迁移到当前的 Java 平台。
Java 环境 - jEnv
如果您在使用 Linux 或 MacOS 的计算机上开发,您可能会考虑使用 jEnv,一种开源 Java 环境管理工具。这是一个命令行工具,所以不要期望 GUI。您可以在以下网址下载该工具。
以下是 Linux 的安装命令:
$ git clone https://github.com/gcuisinier/jenv.git ~/.jenv
要使用 MacOS 和自制软件进行下载,请使用以下命令:
$ 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 的项目。它是 Apache Maven 项目的一部分。如果您还没有使用 Maven 并且进行了大量 Java 开发,那么您可能会被以下 Maven 目标所吸引:
- 简化构建过程
- 提供统一的构建系统
- 提供优质项目信息
- 提供最佳实践开发指南
- 允许透明地迁移到新功能
你可以在这个网站上阅读更多关于 Maven 目标的细节。要下载 Maven,请访问以下网站。此处提供了 Windows、MacOS、Linux 和 Solaris 的安装说明。
Maven 可以与 Eclipse(M2Eclipse)、JetBrains IntelliJ IDEA 和 netbeansIDE 集成。例如,M2Eclipse IDE 提供了与 Apache Maven 的丰富集成,并具有以下特性:
- 您可以从 Eclipse 中启动 Maven 构建
- 您可以管理 Eclipse 构建路径的依赖关系
- 您可以很容易地解析 Maven 依赖关系(您可以直接从 Eclipse 执行此操作,而不必安装本地 Maven 存储库)
- 您可以自动下载所需的依赖项(从远程 Maven 存储库)
- 您可以使用软件向导创建新的 Maven 项目,创建
pom.xml文件,并为普通 Java 项目启用 Maven 支持 - 您可以对 Maven 的远程存储库执行快速的依赖性搜索
获取 Eclipse IDE
要获得 M2EclipseIDE,必须首先安装 Eclipse。步骤如下:
- Start by opening your current Eclipse IDE. Next, select Preferences | Install/Update | Available Software Sites, as shown in the following screenshot:

- The next task is to add the M2Eclipse repository site to your list of Available Software Sites. To accomplish this, click the Add button and enter values in the Name and Location text input boxes. For Name, enter something to help you remember that M2Eclipse is available at this site. For Location, enter the URL: http://download.eclipse.org/technology/m2e/releases. Then, click the OK button:

- You should now see the M2Eclipse site listed in your list of Available Software Sites, as shown in the following screenshot. Your final step is to click the OK button:

- Now, when you start a new project, you will see Maven Project as an option:

总结
在本章中,我们探讨了将现有应用迁移到当前 Java 平台时可能涉及的问题。我们研究了手动和半自动迁移过程,本章为您提供了一些见解和过程,使您的 Java8 代码能够在新的 Java 平台上工作。具体来说,我们对项目 Jigsaw 进行了快速回顾,研究了模块如何适应 Java 环境,提供了迁移规划的技巧,共享了 Oracle 关于迁移的建议,以及可以在开始时使用的共享工具。
在下一章中,我们将详细介绍 JavaShell 和 JShellAPI。我们将演示 JShellAPI 和 JShell 工具以交互方式求值 Java 编程语言的声明、语句和表达式的能力。我们将演示此命令行工具的特性和用法。
问题
- 用新的模块化 Java 平台解决的类路径有什么问题?
- 模块化系统是在哪个版本的 Java 中引入的?
- 模块化系统解决了什么主要问题?
- 总是需要哪个模块?
- 是什么驱动了 Java 中的封装?
- 哪个模块提供对关键内部 API 的访问?
- 可以编辑哪个文件来标识显式命名的模块?
- 什么是 JNLP?
- 下划线作为单个字符标识符的意义是什么?
- 哪三个命令行选项可以用来打破封装?
进一步阅读
此处列出的参考资料将帮助您深入了解本章中介绍的概念:
六、试用 Java Shell
在上一章中,我们探讨了如何将 Java9 之前的应用迁移到新的 Java 平台。我们研究了在 Java9 上运行时可能导致当前应用出现问题的几个问题。我们首先回顾了 Jigsaw 项目,然后研究了模块如何适应新的 Java 平台。我们为您提供了一些见解和过程,使您的 Java8 代码能够与 Java9、10 或 11 一起工作。具体来说,我们提供了迁移规划的技巧、Oracle 关于迁移的共享建议,以及可以用来帮助您开始使用 Java18.x 的共享工具。
在本章中,我们将首先介绍新的命令行,读取求值打印循环(也称为 REPL 工具,以及 Java Shell(JShell)。我们将首先回顾一些关于这个工具的介绍性信息,REPL 概念,然后讨论可以与 JShell 一起使用的命令和命令行选项。我们将采用实践者的方法来回顾 JShell,并包括您可以自己尝试的示例。
本章将讨论以下主题:
- 什么是 JShell?
- JShell 入门
- JShell 的实际应用
- 使用脚本
技术要求
本章以 Java11 为特色,Java 平台的标准版(SE)可从 Oracle 官方下载网站下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
本章的源代码可以在 GitHub 的 URL 上找到。
了解 JShell
JShell 是 Java 平台上比较新的一个重要工具。它是在 JDK9 中引入的。它是一个交互式 REPL 工具,用于求值以下 Java 编程语言组件声明、语句和表达式。它有自己的 API,因此可以被外部应用使用。
读取求值打印循环 通常称为 REPL,取自词组中每个单词的第一个字母。 它也被称为语言外壳或交互式顶层。
JShell 的引入是 JDK 增强建议(JEP)222 的结果。以下是本 JEP 关于 Java Shell 命令行工具的既定目标:
- 便于快速调查
- 便于快速编码
- 提供编辑历史记录
前面列出的快速调查和编码包括语句和表达式。令人印象深刻的是,这些语句和表达式不需要是方法的一部分。此外,变量和方法不需要是类的一部分,这使得这个工具特别动态。
此外,还包括以下列出的功能,以使 JShell 更易于使用,并使您使用 JShell 的时间尽可能节省时间:
- 制表符补全
- 语句结尾分号的自动补全
- 导入的自动补全
- 定义的自动补全
JShell 入门
JShell 是位于/bin文件夹中的命令行工具。此工具的语法如下:
jshell <options> <load files>
正如您在下面的屏幕截图中看到的,有几个选项可用于此工具:

您已经看到了我们使用jshell -h执行的-h选项。这提供了 JShell 选项的列表。
要登录 JShell,只需使用jshell命令即可。您将看到命令窗口中的提示会相应更改:

退出 Shell 就像进入/exit一样简单。进入 JShell 后,可以输入以下任何命令:
| 命令 | 功能 |
|---|---|
/drop |
使用此命令删除被name或id引用的源条目。语法如下:/drop <name or id> |
/edit |
使用此命令,您可以使用name或id引用编辑源条目语法如下:/edit <name or id> |
/env |
这个强大的命令允许您查看或更改求值上下文语法如下:/env [-class-path <path>] [-module-path <path>] [-add-modules <modules>] |
/exit |
此命令用于退出 JShell。语法是简单的/exit,没有任何可用的选项或参数。 |
/history |
history命令提供您所键入内容的历史记录。语法是简单的/history,没有任何可用的选项或参数。 |
/<id> |
此命令用于通过引用id重新运行以前的代码段。语法如下:/<id>您也可以使用/-<n>引用前n个代码段来运行特定的代码段。 |
/imports |
可以使用此命令列出导入的项目。语法为/imports,不接受任何选项或参数。 |
/list |
此命令将列出您键入的源代码。语法如下:/list [<name or id> | -all | -start] |
/methods |
此命令列出所有声明的方法及其签名。语法如下:/methods [<name or id> | -all | -start] |
/open |
使用此命令,可以打开一个文件作为源输入。语法如下:/open <file> |
/reload |
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 | -history | -start] <file> |
/set |
此命令用于设置 JShell 配置信息。语法如下:/set editor | start | feedback | mode | prompt | truncation | format |
/types |
这个命令只列出声明的类型。语法如下:/types [<name or id> | -all | -start] |
/vars |
此命令列出所有声明的变量及其值。语法如下:/vars [<name or id> | -all | -start] |
/! |
此命令将重新运行最后一个代码段。语法很简单/! |
前面列出的几个命令使用术语片段。在 Java 和 JShell 的上下文中,代码段如下所示:
ClassDeclarationExpressionFieldDeclarationImportDeclarationInterfaceDeclarationMethodDeclaration
在 JShell 中输入/help或/?命令提供了一个完整的命令列表和可以在 Shell 中使用的语法。该清单如下:

我们鼓励您尝试使用 JShell 命令。您可以使用前面的屏幕截图来提醒自己正确的语法。
如果您还不熟悉 JShell,/help命令会特别有用。在下面的屏幕截图中可以看到,我们只需输入/help intro命令,就可以获得 JShell 的简介:

如果您发现自己经常使用 JShell,那么您可能会受益于下面列出的一个或多个快捷方式。可以随时从 JShell 中使用/help shortcuts命令列出这些内容:

在 JShell 中,可以使用/help命令,然后使用需要额外帮助的命令来获得额外的帮助。例如,输入/help reload提供有关/reload命令的详细信息。该信息提供如下:

JShell 的实际应用
无论您是一个新的或经验丰富的开发人员,还是刚刚接触 Java,您一定会发现 JShell 非常有用。在本节中,我们将介绍 JShell 的一些实际用途。具体来说,我们将介绍以下内容:
- 反馈模式
- 列出你的素材
- 在 JShell 中编辑
反馈模式
命令行工具通常提供相对稀疏的反馈,以避免屏幕过于拥挤,否则,可能会对开发人员造成麻烦。除了让开发人员能够创建自己的自定义模式之外,JShell 还有几种反馈模式。
如您所见,在下面的截图中,有四种反馈模式:concise、normal、silent、verbose。我们可以输入不带任何参数的/set feedback命令来列出反馈模式以及识别当前的反馈模式。输出的第一行(请参见下面的屏幕截图)显示用于设置反馈模式的命令行命令和参数集:

我们可以通过在启动 JShell 时包含一个选项来决定第一次进入 JShell 时要进入的模式。以下是命令行选项:
| 命令行命令和选项 | 反馈方式 |
|---|---|
jshell -q |
concise |
jshell -n |
normal |
jshell -s |
silent |
jshell -v |
verbose |
您会注意到我们使用-q来表示concise模式,而不是-c。-c选项具有-c<flag>语法,用于将<flag>传递给编译器。有关这些标志的更多信息,请参阅本章“进一步阅读”部分中列出的参考资料。
回顾反馈模式之间的差异最好的方法是使用示例。从normal模式开始,我们将执行命令行命令来完成以下有序反馈演示:
- 创建一个变量。
- 更新变量的值。
- 创建一个方法。
- 更新方法。
- 运行方法。
为了开始我们的第一个测试,我们将在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反馈模式时,我们会收到更多的反馈以及更好的反馈格式。
创建自定义反馈模式
内部反馈模式(normal、concise、silent、verbose不可修改,可自行创建自定义反馈模式。此过程的第一步是复制现有模式。下面的示例演示如何使用/set mode myCustom verbose -command命令字符串将verbose模式复制到myCustom模式:

我们使用了-command选项来确保接收到命令反馈。您可以使用/set命令和以下屏幕截图中列出的选项之一对反馈模式进行各种更改:

作为一个例子,让我们浏览一下truncation设置,该设置要求在每个输出行上显示多少个字符。使用/set truncation命令,如下面的屏幕截图所示,显示当前的截断设置:

如您所见,我们的myCustom反馈模式截断了80。我们用/set truncation myCustom 60命令将其改为60,然后用/set truncation命令进行验证:

正如您在上一个屏幕截图中看到的,基于我们使用的/set truncation myCustom 60JShell 命令,我们的myCustom反馈模式的截断成功地从verbose模式继承的80更改为60。
列出你的素材
有几个 JShell 命令可以方便地列出您创建的素材。使用上一节的反馈演示,我们执行了/vars、/methods和/list命令,分别提供变量、方法和所有源的列表:

我们还可以使用/list -all命令和选项组合来查看 JShell 导入了哪些包。正如您在下面的屏幕截图中看到的,JShell 导入了几个包,使我们在 Shell 中的工作更加方便,从而节省了我们在方法中导入这些标准包的时间:

如果您只想列出启动导入,可以使用/list -start命令和选项组合。正如您在下面的屏幕截图中看到的,每个启动导入都有一个s前缀,并按数字顺序排列:

在 JShell 中编辑
JShell 不是一个全功能的文本编辑器,但是您可以在 Shell 中做一些事情。本节为您提供编辑技术,分为修改文本、基本导航、历史导航和高级编辑命令。
修改文本
默认的文本编辑/输入模式使您键入的文本显示在当前光标位置。当您想删除文本时,有几个选项可供选择。以下是完整的列表:
| 删除动作 | PC 键盘组合 | Mac 键盘组合 |
|---|---|---|
| 删除当前光标位置的字符 | del |
del |
| 删除光标左侧的字符 | backspace |
del |
| 删除从光标位置到行尾的文本 | Ctrl + K |
Cmd + K |
| 删除从光标位置到当前单词末尾的文本 | Alt + D |
Alt + D |
| 从光标位置删除到上一个空白处 | Ctrl + W |
Cmd + W |
| 在光标位置粘贴最近删除的文本 | Ctrl + Y |
Cmd + Y |
当使用Ctrl + Y(或 Macintosh 上的Cmd + Y)时,您将能够使用Alt + Y键盘组合循环浏览先前删除的文本 |
Alt + Y |
Alt + Y |
基本导航
虽然 JShell 中的导航控件与大多数命令行编辑器类似,但有一个基本导航控件列表是很有帮助的:
| 键/键组合 | 导航动作 |
|---|---|
| 向左箭头 | 向后移动一个字符 |
| 向右箭头 | 向前移动一个字符 |
| 向上箭头 | 在历史中向上移动一行 |
| 向下箭头 | 沿着历史向前移动一行 |
| 返回 | 输入(提交)当前行 |
Ctrl + A(Cmd + A在 Macintosh 上) |
跳到当前行的开头 |
Ctrl + E(Cmd + E在 Macintosh 上) |
跳到当前行的末尾 |
Alt + B |
退一步说 |
Alt + F |
向前跳一个字 |
历史导航
JShell 会记住您输入的代码段和命令。它维护此历史记录,以便您可以重用已输入的代码段和命令。要循环浏览代码段和命令,可以按住Ctrl键(Macintosh 上的cmd,然后使用上下箭头键,直到看到所需的代码段或命令。
高级编辑命令
还有几个编辑选项可用,以便您可以包括搜索功能、宏创建和使用等。JShell 的编辑器基于 JLine2,这是一个用于解析控制台输入和编辑的 Java 库。您可以在这里了解更多关于 JLine2 的信息。
使用脚本
到目前为止,您已经从键盘将数据直接输入 JShell。现在您可以使用 JShell 脚本了,它是一系列 JShell 命令和代码段。该格式与其他脚本格式相同,每行一条命令。
在本节中,我们将介绍启动脚本,研究如何加载脚本,如何保存脚本,最后介绍使用 JShell 编写高级脚本。
启动脚本
每次启动 JShell 时,都会加载启动脚本。每次使用/reset、/reload和/env命令时也会发生这种情况。
默认情况下,DEFAULT启动脚本由 JShell 使用。如果你想使用不同的启动脚本,你只需要使用/set start <script>命令。举个例子:
/set start MyStartupScript.jsh
或者,您可以在命令提示符处使用 JShellstart MyStartupScript.jsh命令来启动 JShell 并加载MyStartupScript.jshJShell 启动脚本。
当您使用带有-retain选项的/set start <script>命令时,您告诉 JShell 在下次启动 JShell 时使用新的启动脚本。
加载脚本
在 JShell 中加载脚本可以通过以下方法之一完成:
- 您可以使用
/open命令和脚本名称作为参数。例如,如果我们的脚本名是MyScript,我们将使用/open MyScript。 - 加载脚本的第二个选项是在命令提示符处使用
jshell MyScript.jsh命令。这将启动 JShell 并加载MyScript.jshJShell 脚本。
保存脚本
除了在外部编辑器中创建 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 文件。我们鼓励你自己尝试一下,这样你就可以熟悉这个过程了。
总结
在本章中,我们研究了 Java 的 REPL 命令行工具 JShell,我们从有关该工具的介绍性信息开始,并仔细地研究了 REL 概念。我们花了相当长的时间来查看 JShell 命令和命令行选项。我们的报道包括反馈模式、素材清单和 Shell 中编辑的实用指南。我们还获得了脚本工作经验
在下一章中,我们将介绍 Java 的默认垃圾收集器。具体来说,我们将查看默认的垃圾收集、已废弃的垃圾收集组合,并检查垃圾收集日志记录。
问题
- 什么是 REPL?
- 什么是 JShell?
- 您能说出 JShell 的四个最新特性,这些特性使其使用更加高效?
- JShell 在您的计算机文件系统中的位置是什么?
- 你怎么离开 JShell?
- 您将使用哪个 JShell 命令列出所有声明的变量及其对应的值?
- 如何获得可以与 JShell 一起使用的命令和语法的完整列表?
- 如何获得有关特定 JShell 命令的详细帮助?
- 什么是反馈模式?
- 什么是默认的反馈模式?
进一步阅读
下面这本书是了解 JShell 的好资料:
- 《Java9 和 JShell》在这个页面上提供。
七、利用默认的 G1 垃圾收集器
在上一章中,我们研究了 Java Shell(JShell)、Java 的 读取求值打印循环(REPL)命令行工具。我们从介绍该工具的信息开始,仔细研究了 REPL 概念。我们花了大量时间来检查 JShell 命令和命令行选项。我们的报道包括反馈模式的实用指南、素材列表和 Shell 中的编辑。我们还获得了使用脚本的经验。
在本章中,我们将深入了解垃圾收集以及如何在 Java 中处理它。我们将从垃圾收集的概述开始,然后看看 Java9 之前的领域中的细节。有了这些基本信息,我们将研究 Java9 平台中特定的垃圾收集更改。最后,我们将研究一些即使在 Java11 之后仍然存在的垃圾收集问题。
本章包括以下主题:
- 垃圾收集概述
- Java9 之前的垃圾收集模式
- 用新的 Java 平台收集垃圾
- 长期存在的问题
技术要求
本章主要介绍 Java11。Java 平台的标准版(SE)可以从 Oracle 的官方下载站点下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
垃圾收集概述
垃圾收集是 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 的垃圾收集可以避免这些泄漏。
垃圾收集算法
JVM 可以使用几种垃圾收集算法或类型。在本节中,我们将介绍以下垃圾收集算法:
- 标记和扫描
- 并发标记扫描(CMS)垃圾收集
- 串行垃圾收集
- 并行垃圾收集
- G1 垃圾收集
标记和扫描
Java 的初始垃圾收集算法标记清除使用了一个简单的两步过程:
-
第一步,标记,是遍历所有具有可访问引用的对象,将这些对象标记为活动对象
-
第二步,扫描,包括扫描海洋中任何没有标记的对象
正如您可以很容易地确定的那样,标记和扫描算法似乎很有效,但由于这种方法的两步性质,它可能不是很有效。这最终导致了一个 Java 垃圾收集系统,大大提高了效率。
并发标记扫描(CMS)垃圾收集
用于垃圾收集的 CMS 算法使用多个线程扫描堆内存。与“标记并扫描”方法类似,它标记要删除的对象,然后进行扫描以实际删除这些对象。这种垃圾收集方法本质上是一种升级的标记和扫描方法。它进行了修改,以利用更快的系统和性能增强。
要为应用手动调用 CMS 垃圾收集算法,请使用以下命令行选项:
-XX:+UseConcMarkSweepGC
如果要使用 CMS 垃圾收集算法并指定要使用的线程数,可以使用以下命令行选项。在下面的示例中,我们告诉 Java 平台使用带有八个线程的 CMS 垃圾收集算法:
-XX:ParallelCMSThreads=8
串行垃圾收集
Java 的串行垃圾收集在一个线程上工作。执行时,它冻结所有其他线程,直到垃圾收集操作结束。由于串行垃圾收集的线程冻结性质,它只适用于非常小的程序
要手动调用应用的串行垃圾收集算法,请使用以下命令行选项:
-XX:+UseSerialGC
并行垃圾收集
在 Java8 和更早版本中,并行垃圾收集算法是默认的垃圾收集器。它使用多个线程,但冻结应用中的所有非垃圾收集线程,直到垃圾收集函数完成,就像串行垃圾收集算法一样。
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 |
| 此标志将单个幸存者空间的大小确定为伊甸园空间大小的一部分。 | -XX:SurvivorRatio=15 |
| 此标志确定永久空间的初始大小。 | -XX:PermSize=512m |
| 此标志确定永久空间的最大大小。 | -XX:MaxPermSize=512m |
| 此标志确定每个线程专用的栈区域的大小(以字节为单位)。 | -Xss512k |
| 此标志确定每个线程专用的栈区域的大小(以 KB 为单位)。 | -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(简称线程本地分配块)。 | -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 循环开始之前分配的 CMS 生成中MinHeapFreeRatio的百分比。 |
-XX:CMSTriggerRatio=70 |
设置在开始 CMS 收集循环之前分配的 CMS 永久生成中MinHeapFreeRatio的百分比。 |
-XX:CMSTriggerPermRatio=90 |
| 这是触发 CMS 集合后的等待时间。使用参数指定允许 CMS 等待年轻集合的时间。 | -XX:CMSWaitDuration=2000 |
| 启用平行备注。 | -XX:+CMSParallel\RemarkEnabled |
| 启用幸存者空间的平行备注。 | -XX:+CMSParallel\SurvivorRemarkEnabled |
| 您可以使用此命令在备注阶段之前强制年轻的集合。 | -XX:+CMSScavengeBeforeRemark |
| 如果使用的 Eden 低于阈值,则使用此选项可防止出现计划注释。 | -XX:+CMSScheduleRemark\EdenSizeThreshold |
| 设置您希望 CMS 尝试和安排备注暂停的 Eden 占用百分比。 | -XX:CMSScheduleRemark\EdenPenetration=20 |
| 至少在新生代的入住率达到您想要安排备注的 1/4(在我们右边的示例中)之前,您就要在这里开始对 Eden 顶部进行采样。 | -XX:CMSScheduleRemark\SamplingRatio=4 |
备注后可选择variant=1或variant=2验证。 |
-XX:CMSRemarkVerifyVariant=1 |
| 选择使用并行算法进行年轻空间的收集。 | -XX:+UseParNewGC |
| 允许对并发阶段使用多个线程。 | -XX:+CMSConcurrentMTEnabled |
| 设置用于并发阶段的并行线程数。 | -XX:ConcGCThreads=2 |
| 设置要用于停止世界阶段的并行线程数。 | -XX:ParallelGCThreads=2 |
| 您可以启用增量 CMS(iCMS)模式。 | -XX:+CMSIncrementalMode |
| 如果未启用,CMS 将不会清理永久空间。 | -XX:+CMSClassUnloadingEnabled |
这允许System.gc()触发并发收集,而不是整个垃圾收集周期。 |
-XX:+ExplicitGCInvokes\Concurrent |
这允许System.gc()触发永久空间的并发收集。 |
‑XX:+ExplicitGCInvokes\ConcurrentAndUnloadsClasses |
iCMS 模式适用于 CPU 数量少的服务器。 不应在现代硬件上使用它。
以下是一些杂项垃圾收集选项:
| 其他垃圾收集选项 | 标志 |
|---|---|
这将导致 JVM 忽略应用的任何System.gc()方法调用。 |
-XX:+DisableExplicitGC |
| 这是堆中每 MB 可用空间的生存时间(软引用),以毫秒为单位。 | -XX:SoftRefLRU\PolicyMSPerMB=2000 |
这是用于在抛出OutOfMemory错误之前限制垃圾收集所用时间的使用策略。 |
-XX:+UseGCOverheadLimit |
这限制了抛出OutOfMemory错误之前在垃圾收集中花费的时间比例。与GCHeapFreeLimit一起使用。 |
-XX:GCTimeLimit=95 |
这将设置在抛出OutOfMemory错误之前,完全垃圾收集之后的最小可用空间百分比。与GCTimeLimit一起使用。 |
-XX:GCHeapFreeLimit=5 |
最后,这里有一些特定于 G1 的选项。请注意,从 jvm6u26 开始,所有这些都受支持:
| G1 垃圾收集选项 | 标志 |
|---|---|
| 堆区域的大小。默认值是 2048,可接受的范围是 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,使用返回单例的Runtime myRuntime = Runtime.getRuntime();。这使我们能够访问 JVM。在打印一些头信息和初始内存统计信息之后,我们创建了大小为300000的ArrayList。然后,我们创建一个循环来生成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 5 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.");
}
}
}
正如您在前面的代码中所看到的,objectTally计数在每次创建类型为Animal的对象时递增,而在垃圾收集器删除类型为Animal的对象时递减。
通常不鼓励覆盖对象的finalize()方法。finalize()方法通常应声明为protected。
Java9 之前的垃圾收集模式
Java 的垃圾收集对于 Java9 来说并不新鲜,它从 Java 的初始版本就已经存在了,Java 早就有了一个复杂的垃圾收集系统,它是自动的并且在后台运行。通过在后台运行,我们指的是在空闲时间运行的垃圾收集进程。
空闲时间是指输入/输出之间的时间,例如键盘输入、鼠标单击和输出生成之间的时间。
这种自动垃圾收集是开发人员选择 Java 作为编程解决方案的关键因素之一。其他编程语言,如 C#和 Objective-C,在 Java 平台成功之后已经实现了垃圾收集。
在查看当前 Java 平台中对垃圾收集的更改之前,下面让我们先看看下面列出的概念:
- 可视化垃圾收集
- Java8 中的垃圾收集升级
- 用 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.
可能不清楚的是,示例代码产生了五个未引用的字符串对象,这在一定程度上是由于字符串是不可变的。如下表所示,对于每一行连续的代码,被引用的对象都会被更新,而另一个对象将变为未被引用:

未引用对象累积
前面列出的未引用对象肯定不会破坏内存库,但它表示大量未引用对象的累积速度有多快。
Java8 中的垃圾收集升级
从 Java8 开始,默认的垃圾收集算法是并行垃圾收集器。这些改进之一是能够使用以下命令行选项通过删除重复的字符串值来优化堆内存:
-XX:+UseStringDeduplication
G1 垃圾收集器在看到字符串时可以查看字符数组。然后,它获取值并将其与新的、弱的字符数组引用一起存储。如果 G1 垃圾收集器发现一个具有相同哈希码的字符串,它将用一个字符一个字符的检查来比较这两个字符串。如果找到匹配项,两个字符串最终都指向同一个字符数组。具体来说,第一个字符串将指向第二个字符串的字符数组。
这种方法可能需要大量的处理开销,只有在认为有益或绝对必要时才应使用。
案例研究-用 Java 编写的游戏
多人游戏需要广泛的管理技术,无论是服务器还是客户端系统。JVM 在低优先级线程中运行垃圾收集线程,并定期运行。服务器管理员以前使用了一个增量垃圾收集模式,使用现在已废弃的-Xincgc命令行选项,以避免服务器过载时发生服务器暂停。目标是让垃圾收集运行得更频繁,每次执行周期要短得多。
在考虑内存使用和垃圾收集时,在目标系统上使用尽可能少的内存并在可行的范围内限制垃圾收集的暂停是很重要的。这些技巧对于游戏、模拟和其他需要实时性能的应用尤其重要。
JVM 管理存储 Java 内存的堆。默认情况下,JVM 从一个小堆开始,随着其他对象的创建而增长。堆有两个分区:年轻分区和终身分区。最初创建对象时,它们在年轻分区中创建。持久对象被移动到保留分区。对象的创建通常非常快速,只需增加指针即可。年轻分区的处理速度比长期分区快得多。这是很重要的,因为它适用于整个应用,或者在我们的情况下,一个游戏的效率。
对我们来说,监控游戏的内存使用情况以及垃圾收集发生的时间变得非常重要。为了监控垃圾收集,我们可以在启动游戏时添加verbose标志(-verbose:gc),例如下面的例子:
java -verbose:gc MyJavaGameClass
然后 JVM 将为每个垃圾收集提供一行格式化输出。以下是verboseGC 输出的格式:
[<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,从 Release9 开始一直到 Release11,其中包括对垃圾收集系统的一些相关更改。让我们回顾一下最重要的变化:
- 默认垃圾收集
- 废弃的垃圾收集组合
- 统一垃圾收集日志
- 垃圾收集接口
- G1 的并行完全垃圾收集
- Epsilon:一个任意低开销的垃圾收集(GC)
我们将在下面的小节中回顾每一个垃圾收集概念问题。
默认垃圾收集
我们之前详细介绍了 Java9 之前的 JVM 使用的以下垃圾收集方法。这些仍然是合理的垃圾收集算法:
- CMS 垃圾收集
- 串行垃圾收集
- 并行垃圾收集
- G1 垃圾收集
让我们简要回顾一下这些方法:
- CMS 垃圾收集:CMS 垃圾收集算法使用多线程扫描堆内存。使用这种方法,JVM 标记要删除的对象,然后进行扫描以实际删除它们。
- 串行垃圾收集:这种方法在单个线程上使用线程冻结模式。当垃圾收集正在进行时,它会冻结所有其他线程,直到垃圾收集操作结束。由于串行垃圾收集的线程冻结特性,它只适用于非常小的程序。
- 并行垃圾收集:这种方法使用多个线程,但冻结应用中所有非垃圾收集线程,直到垃圾收集函数完成,就像串行垃圾收集算法一样
- G1 垃圾收集:这是垃圾收集算法,具有以下特点:
- 与大内存堆一起使用
- 包括将内存堆分割为多个区域
- 与每个堆区域并行进行
- 释放内存时压缩堆空间
- 使用停止世界方法进行压实操作
- 根据要收集的垃圾最多的区域来确定区域的优先级
在 Java9 之前,并行垃圾收集算法是默认的垃圾收集器,在 Java9 中,G1 垃圾收集器是 Java 内存管理系统的新默认实现。32 位和 64 位服务器配置都是如此
Oracle 评估 G1 垃圾收集器,主要是由于它的低暂停特性,是一种比并行方法性能更好的垃圾收集方法。这一变化基于以下概念:
- 限制延迟是很重要的
- 最大化吞吐量不如限制延迟重要
- G1 垃圾收集算法是稳定的
使 G1 垃圾收集方法成为并行方法的默认方法涉及两个假设:
- 使 G1 成为默认的垃圾收集方法将显著增加其使用量。这种增加的使用可能会暴露出在 Java9 之前没有意识到的性能或稳定性问题
- G1 方法比并行方法更需要处理器。在某些用例中,这可能有点问题。
从表面上看,这一变化对于 Java9 来说似乎是一个伟大的进步,很可能就是这样。但是,当盲目接受这种新的默认收集方法时,应该谨慎使用。如果切换到 G1,建议对系统进行测试,以确保应用不会因使用 G1 而出现性能下降或意外问题。如前所述,G1 并没有从并行方法的广泛测试中获益。
关于缺乏广泛测试的最后一点意义重大。使用 Java9 将 G1 作为默认的自动内存管理(垃圾收集)系统等同于将开发人员变成毫无戒备的测试人员。虽然预计不会出现大的问题,但了解到在使用 G1 和 Java9 时可能会出现性能和稳定性问题,将更加强调测试 Java9 应用。
废弃的垃圾收集组合
Oracle 在将特性、API 和库从 Java 平台的新版本中删除之前,一直非常重视这些特性、API 和库。有了这个模式,在 Java8 中被贬低的语言组件就可以在 Java9 中被删除。在 Java8 中,有一些垃圾收集组合被认为很少使用和被贬低
下面列出的这些组合已在 Java9 中删除:
- DefNew + CMS
- ParNew + SerialOld
- 增量 CMS
这些组合除了很少使用之外,还为垃圾收集系统带来了不必要的复杂性。这导致了系统资源的额外消耗,而没有为用户或开发人员提供相应的好处
以下列出的垃圾收集配置受 Java8 平台中上述废弃的影响:
| 垃圾收集配置 | 标志 |
|---|---|
| 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 |
随着 Java9 的发布,JDK8 中的垃圾收集组合被删除,这些组合与控制这些组合的标志一起列出。此外,启用 CMS 前台集合的标志已被删除,并且在 JDK9 中不存在。这些标志如下所示:
| 垃圾收集组合 | 标志 |
|---|---|
| CMS 前景 | -XX:+UseCMSCompactAtFullCollection |
| CMS 前景 | -XX+CMSFullGCsBeforeCompaction |
| CMS 前景 | -XX+UseCMSCollectionPassing |
删除已废弃的垃圾收集组合的唯一缺点是,使用带有本节中列出的任何标志的 JVM 启动文件的应用将需要修改其 JVM 启动文件以删除或替换旧标志。
统一垃圾收集日志记录
统一 GC 日志记录是 JDK9 增强的一部分,旨在使用统一 JVM 日志记录框架重新实现垃圾收集日志记录。因此,让我们首先回顾一下统一 JVM 日志记录计划。
统一 JVM 日志记录
为 JVM 创建统一的日志模式包括以下目标的高级列表:
- 为所有日志操作创建一组 JVM 范围的命令行选项。
- 使用分类标签进行日志记录。
- 提供六个级别的日志记录,如下所示:
- 错误
- 警告
- 信息
- 调试
- 跟踪
- 开发
这不是一个详尽的目标清单。我们将在第 14 章“命令行标志”中更详细地讨论 Java 的统一日志模式。
在日志记录上下文中,对 JVM 的更改可以分为:
- 标签
- 水平
- 装饰
- 输出
- 命令行选项
让我们简单地看一下这些类别。
标签
日志标记在 JVM 中标识,如果需要,可以在源代码中更改。标签应该是自识别的,例如用于垃圾收集的gc。
级别
每个日志消息都有一个关联的级别。如前所列,级别包括错误、警告、信息、调试、跟踪和开发。下图显示了级别的详细程度如何随着记录的信息量的增加而增加:

冗长程度
装饰
在 Java 日志框架的上下文中,装饰是关于日志消息的元数据。以下是按字母顺序排列的可用装饰品列表:
levelpidtagstidtimetimemillistimenanosuptimeuptimemillisuptimenanos
有关这些装饰的说明,请参阅第 14 章、“命令行标志”。
输出
Java9 日志框架支持三种类型的输出:
stderr:向stderr提供输出stdout:向stdout提供输出- 文本文件:将输出写入文本文件
命令行选项
通过命令行控制 JVM 的日志操作。-Xlog命令行选项有大量的参数和可能性。下面是一个例子:
-Xlog:gc+rt*=debug
在本例中,我们告诉 JVM 执行以下操作:
- 记录至少带有
gc和rt标记的所有消息 - 使用
debug水平 - 向
stdout提供输出
统一 GC 日志记录
现在我们已经对 Java 的日志框架的变化有了大致的了解,让我们看看引入了哪些变化。在本节中,我们将了解以下方面:
- 垃圾收集日志记录选项
gc标签- 宏
- 其他注意事项
垃圾收集日志记录选项
下面是我们在引入 Java 日志框架之前可以使用的垃圾收集日志选项和标志的列表:
| 垃圾收集日志记录选项 | 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 |
| 每次停止暂停后,打印暂停摘要。 | -XX:+PrintGC\ApplicationStoppedTime |
| 此标志将打印垃圾收集的每个并发阶段的时间。 | -XX:+PrintGC\ApplicationConcurrentTime |
| 使用此标志将在完全垃圾收集后打印类直方图。 | -XX:+Print\ClassHistogramAfterFullGC |
| 使用此标志将在完全垃圾收集之前打印类直方图。 | -XX:+Print\ClassHistogramBeforeFullGC |
| 这将在完全垃圾收集之后创建一个堆转储文件。 | -XX:+HeapDump\AfterFullGC |
| 这将在完全垃圾收集之前创建一个堆转储文件。 | -XX:+HeapDump\BeforeFullGC |
| 这将在内存不足的情况下创建堆转储文件。 | -XX:+HeapDump\OnOutOfMemoryError |
| 您可以使用此标志指定要在系统上保存堆转储的路径。 | -XX:HeapDumpPath=<path> |
如果n >= 1,您可以使用它来打印 CMS 统计信息。仅适用于 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标签
我们可以使用带有-Xlog选项的gc标记来通知 JVM 在info级别只记录gc标记的项。您还记得,这类似于使用-XX:+PrintGC。使用这两个选项,JVM 将为每个垃圾收集操作记录一行。
值得注意的是,gc标签并非单独使用,而是建议与其他标签一起使用。
宏
我们可以创建宏,以向垃圾收集日志记录添加逻辑。以下是log宏的一般语法:
log_<level>(Tag1[,...])(fmtstr, ...)
以下是一个log宏的例子:
log_debug(gc, classloading)("Number of objects loaded: %d.", object_count)
下面的示例框架log宏显示了如何使用新的 Java 日志框架来创建脚本,以提高日志记录的逼真度:
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 垃圾收集的改进并没有随着 Java8 和 Java9 中的主要变化而停止。在 Java10 中,引入了一个干净的垃圾收集器接口。新接口的目标是增加特定于 HotSpot JVM 的内部垃圾收集代码的模块化。增加的模块化将使新接口更容易更新,而不会对核心代码库产生负面影响。另一个好处是相对容易地从 JDK 构建中排除垃圾收集。
在 Java10 之前,垃圾收集实现在 JVM 的整个文件结构中都是源代码。清理这些代码以使代码模块化是优化 Java 代码库和使垃圾收集现代化的一个自然步骤,这样可以更容易地更新和使用。
在 Java 中,垃圾收集器实现了CollectedHeap类,该类管理 JVM 和垃圾收集操作之间的交互
新的垃圾收集接口值得注意,但最适用于垃圾收集和 JVM 开发人员
G1 的并行完全垃圾收集
正如本章前面提到的,G1 垃圾收集器自 Java9 以来一直是默认的垃圾收集器。G1 垃圾收集器的效率之一是它使用并发垃圾收集而不是完全收集。有时会实现完全垃圾收集,通常是并发垃圾收集速度不够快。注意,在 Java9 之前,并行收集器是默认的垃圾收集器,是一个并行的完全垃圾收集器。
对于 Java10,G1Full 垃圾收集器被转换为并行,以减轻对使用完全垃圾收集的开发人员的任何负面影响。将用于 G1 完全垃圾收集的 mark-week 压缩算法并行化。
Epsilon–任意低开销 GC
Java 的最新版本 11 附带了一个负责内存分配的被动 GC。这个 GC 的被动性质(称为 EpsilonGC)表明它不执行垃圾收集;相反,它继续分配内存,直到堆上没有剩余空间为止。这时,JVM 关闭。
为了启用 Epsilon GC,我们使用以下任一方法:
-XX:+UseEpsilonGC-XX:+UseNoGC
EpsilonGC 的使用主要出现在测试中,由于缺乏垃圾收集,它的开销很低,提高了测试效率
长期存在的问题
即使有了 Java9、10 和 11 的现代版本,Java 的垃圾收集系统也有缺点,因为它是一个自动过程,所以我们不能完全控制收集器的运行时间。作为开发人员,我们不能控制垃圾收集,JVM 是。JVM 决定何时运行垃圾收集。正如您在本章前面所看到的,我们可以要求 JVM 使用System.gc()方法运行垃圾收集。尽管我们使用了这种方法,但我们不能保证我们的请求会得到满足,也不能保证我们的请求会及时得到满足
在本章前面,我们回顾了垃圾收集的几种方法和算法。我们讨论了作为开发人员如何控制流程。这假设我们有能力控制垃圾收集。即使我们指定了一种特定的垃圾收集技术(例如,将-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类的实例创建额外的引用变量之外,没有做任何有趣的事情。在我们的示例中,我们将objectNbr2、objectNbr3、objectNbr4和objectNbr5引用设置为null。尽管这些对象有实例变量并且可以相互引用,但是通过将它们的引用设置为null,它们在类之外的可访问性已经终止。这使得它们(objectNbr2、objectNbr3、objectNbr4和objectNbr5有资格进行垃圾收集:
package MyGarbageCollectionSuite;
public class GarbageCollectionExperimentThree
{
// 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 平台组件。我们的综述包括对象生命周期、垃圾收集算法、垃圾收集选项以及与垃圾收集相关的方法。我们研究了 Java8、9、10 和 11 中对垃圾收集的升级,并研究了一个案例来帮助我们理解现代垃圾收集。
然后,我们将重点转向新的 Java9 平台对垃圾收集的更改。我们在 Java 中对垃圾收集的探索包括默认垃圾收集、废弃的垃圾收集组合和统一的垃圾收集日志记录。我们通过查看一些即使在最新版本的 Java 中仍然存在的垃圾收集问题来结束对垃圾收集的探索。
在下一章中,我们将研究如何使用 Java 微基准线束(JMH)编写性能测试,这是一个用于编写 JVM 基准测试的 Java 线束库。
问题
- 列举五种垃圾收集算法。
- 什么是 G1?
- iCMS 的用途是什么?
- 什么是 MiB?
- 如何显式调用垃圾收集?
- 如何将
finalize()方法添加到自定义对象? - 以下垃圾收集组合有什么共同点?
- DefNew + CMS
- ParNew + Serial
- 旧的增量 CMS
- 在 Java 中,由垃圾收集器实现的哪个类管理 JVM 和垃圾收集操作之间的交互?
- Java10 中对 g1fullgc 做了哪些更改?
- Java11 中引入的被动 GC 的名称是什么?
进一步阅读
以下参考资料将帮助您深入了解本章中介绍的概念:
- 《Java EE 8 高性能》【视频】在这个页面提供。
八、JMH 的微基准应用
在上一章中,我们深入回顾了垃圾收集,包括对象生命周期、垃圾收集算法、垃圾收集选项以及与垃圾收集相关的方法。我们简要介绍了 Java8 中垃圾收集的升级,重点介绍了新 Java 平台的变化。我们对 Java11 中的垃圾收集的探索包括:默认垃圾收集、废弃的垃圾收集组合、统一的垃圾收集日志记录以及持久存在的垃圾收集问题。
在本章中,我们将研究如何使用 Java 微基准线束(JMH)编写性能测试,这是一个用于编写 JVM 基准测试的 Java 线束库。我们将使用 Maven 和 JMH 来帮助说明使用新 Java 平台进行微标记的威力。
具体来说,我们将讨论以下主题:
- 微基准概述
- Maven 微基准
- 基准选择
- 避免微基准陷阱的技术
技术要求
本章以 Java11 为特色,Java 平台的标准版(SE)可从 Oracle 官方下载网站下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
本章的源代码可以在 GitHub 的 URL 上找到。
微基准概述
微基准是用来测试系统性能的。这与宏观基准测试不同,后者在不同的平台上运行测试,以进行效率比较和后续分析。使用微标记,我们通常针对一个系统上的特定代码片段,例如方法或循环。微基准的主要目的是在我们的代码中识别优化机会。
基准测试有多种方法;我们将重点介绍如何使用 JMH 工具?开发人员并不总是关心性能问题,除非性能是一个明确的要求。这可能会导致部署后的意外情况,如果将微基准作为开发过程的一部分进行,则可以避免这些意外情况。
微基准发生在一个过程的几个阶段。如下图所示,流程包括设计、实现、执行、分析和增强:

微基准过程阶段
在设计阶段,我们确定了我们的目标并设计了相应的微基准;在实现阶段,我们编写了微基准,然后在执行阶段,我们实际运行了测试。在分析阶段,我们利用手中的微标记结果对结果进行了解释和分析。这导致了增强阶段的代码改进。一旦我们的代码被更新,我们就重新设计微基准测试,调整实现,或者直接进入执行阶段。这是一个循环的过程,一直持续到我们实现目标中确定的性能优化为止。
使用 JMH 的方法
Oracle 的文档表明,最理想的 JMH 用例是使用依赖于应用 JAR 文件的 Maven 项目。他们进一步建议微标记通过命令行进行,而不是从 IDE 中进行,因为这可能会影响结果。
Maven,也称为 ApacheMaven,是一个项目管理和理解工具,我们可以使用它来管理我们的应用项目构建、报告和文档。
为了使用 JMH,我们将使用字节码处理器(注解)来生成基准代码。
为了测试 JMH,您必须有一个支持 Maven 的 IDE 和您正在使用的 Java 版本。如果您还没有 Java11 或支持 Java11 的 IDE,可以按照下一节中的步骤操作。
安装 Java 和 Eclipse
您可以从 JDK11 早期访问构建页面下载并安装。
一旦安装了 Java11,请下载最新版本的 Eclipse。在写这本书的时候,那是氧气。这是相关链接。
动手实验
现在我们已经安装了 EclipseOxygen,您可以运行一个快速测试来确定 JMH 是否在您的开发计算机上工作。首先创建一个新的 Maven 项目,如以下屏幕截图所示:

新 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.openjdk.jmh.Main;
public class Test {
public static void main(String[] args) {
Main.main(args);
}
}
我们现在可以编译和运行我们非常简单的测试程序。结果在控制台选项卡中提供,如果使用命令行,则在实际控制台中提供。以下是您将看到的内容:

JMH 测试结果
Maven 微基准
开始使用 JMH 的一种方法是使用 JMHMaven 原型。第一步是创建一个新的 JMH 项目。在我们系统的命令提示符下,我们将输入mvn命令,然后输入一组长参数,以创建一个新的 Java 项目和必要的 Mavenpom.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命令和前面的详细参数,您将看到通过终端向您报告的结果。根据您的使用级别,您可能会看到大量来自这个页面的下载和其他类似的存储库站点。
您还将看到一个信息部分,通知您项目构建过程,如以下屏幕截图所示:

Maven 构建过程
可能会有额外的插件和从这个页面下载的其他资源仓库。然后,您将看到一个信息反馈组件,它让您知道项目是以批处理模式生成的,如下所示:

Maven 项目生成
最后,您将看到一组参数,并注意到您的项目构建是成功的。正如您在下面的示例中所看到的,该过程用了不到 21 秒的时间完成:

新 Maven 项目
我们将根据-DartifactId选项中包含的参数创建一个文件夹,在我们的示例中,我们使用了-DartifactId=chapter8-benchmark,Maven 创建了一个chapter8-benchmark项目文件夹,如下所示:

基准项目文件夹
您将看到 Maven 创建了pom.xml文件以及一个源(src文件夹。在该文件夹中,C:\chapter8-benchmark\src\main\java\com\packt的子目录结构下是MyBenchmark.java文件。Maven 为我们创建了一个基准类,如下图所示:

MyBenchmark.java文件位置
以下是 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。
您将看到多个存储库下载、源代码编译、插件安装,最后还有Build Success指示符,如下所示:

生成结果
现在您将在项目目录中看到.classpath和.project文件以及新的.settings和target子文件夹,如下所示:

项目目录
如果您导航到\target子文件夹,您将看到我们的benchmarks.jar文件已创建。这个 JAR 包含我们运行基准测试所需的内容。
benchmarks.jar中的外部依赖在pom.xml文件中配置。
我们可以在 IDE 中更新我们的MyBenchmark.java文件,比如 Eclipse。然后,我们可以再次执行mvn clean install来覆盖我们的文件。在初始执行之后,我们的构建速度会更快,因为不需要下载任何东西。
以下是初始执行后构建过程的输出:

清洁安装过程
最后一步是从C:\chapter8-benchmark\target文件夹运行基准工具。我们可以通过以下命令-java -jar benchmarks.jar来完成。即使对于简单代码上的小型基准测试(如我们的示例所示),运行基准测试也可能需要一些时间。可能会有几个迭代,包括热身,以提供更简洁有效的基准测试结果集
这里提供了我们的基准测试结果。如您所见,测试运行了00:08:08个小时:

MyBenchmark.java文件位置
基准选择
在上一节中,您学习了如何运行基准测试。在本节中,我们将查看以下用于运行基准测试的可配置选项:
- 模式
- 时间单位
模式
在上一节中,我们的基准测试结果的输出包括一个Mode列,该列的值为thrpt,是吞吐量的缩写。这是默认模式,另外还有四种模式。所有 JMH 基准模式如下所示:
| 模式 | 说明 |
|---|---|
| 全部 | 依次测量所有其他模式。 |
| 平均时间 | 此模式测量单个基准运行的平均时间。 |
| 采样时间 | 此模式测量基准执行时间,包括最小和最大时间。 |
| 单发时间 | 在这种模式下,没有 JVM 预热,测试是确定单个基准测试方法运行所需的时间。 |
| 吞吐量 | 这是默认模式,测量每秒的操作数。 |
要指定使用哪种基准模式,您需要将@Benchmark代码行修改为以下代码之一:
@Benchmark @BenchmarkMode(Mode.All)@Benchmark @BenchmarkMode(Mode.AverageTime)@Benchmark @BenchmarkMode(Mode.SampleTime)@Benchmark @BenchmarkMode(Mode.SingleShotTime)@Benchmark @BenchmarkMode(Mode.Throughput)
时间单位
为了在基准输出中获得更高的保真度,我们可以指定一个特定的时间单位,从最短到最长列出:
NANOSECONDSMICROSECONDSMILLISECONDSSECONDSMINUTESHOURSDAYS
为了进行此指定,我们只需在@Benchmark行中添加以下代码:
@Benchmark @BenchmarkMode(Mode.AverageTime)
@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。
问题是,为了消除死代码,有时可以删除基准测试代码。
常量折叠是编译时约束被实际结果替换时发生的编译器操作。编译器执行常量折叠以删除任何冗余的运行时计算。在下面的例子中,我们根据涉及第一个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 是一个 Java 工具库,用于为 JVM 编写基准测试。我们尝试使用 Maven 和 JMH 编写性能测试,以帮助说明使用新 Java 平台进行微基准标记的过程。我们从微基准概述开始,然后与 Maven 深入到微基准,回顾了基准选项,最后介绍了一些避免微基准陷阱的技术。
在下一章中,我们将学习编写一个管理其他进程并利用 Java 平台的现代进程管理 API 的应用。
问题
- 什么是微基准?
- 微基准的主要阶段是什么?
- 什么是 Maven?
- 什么文件用于定义依赖关系?
- 关于基准测试,模式和时间单位有什么共同点?
- 什么是 JMH 基准模式?
- 基准测试中使用的时间单位是什么,按从最小到最大的顺序排列?
- 有什么建议策略可以避免电源管理陷阱?
- 为避免操作系统调度器陷阱,有哪些建议策略?
- 避免分时陷阱的建议策略是什么?
进一步阅读
下面列出的参考资料将帮助您深入了解本章介绍的概念:
九、利用进程 API
在上一章中,我们了解到,Java 微基准线束(JMH)是一个 Java 线束库,用于为 JVM 编写基准测试。我们尝试使用 Maven 和 JMH 编写性能测试,以帮助说明使用新 Java 平台进行微基准标记的过程。我们从微基准概述开始,然后与 Maven 深入到微基准,回顾了基准选项,最后介绍了一些避免微基准陷阱的技术。
在本章中,我们将重点介绍对Process类和java.lang.ProcessHandleAPI 的更新。在 Java 的早期版本中,在 Java9 之前,用 Java 管理进程是很困难的。API 不够,有些功能不够,有些任务需要以特定于系统的方式来解决。例如,在 Java8 中,让进程访问自己的进程标识符(PID)是一项不必要的困难任务。
在本章中,我们将探讨编写一个利用 Java 的进程管理 API 管理其他进程的应用所需的必要知识。具体来说,我们将介绍以下内容:
- 进程简介
- 使用
ProcessHandle接口 - 查看示例进程控制器应用
技术要求
本章以 Java11 为特色,Java 平台的标准版(SE)可从 Oracle 官方下载网站下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
本章的源代码可以在 GitHub 的 URL 上找到。
进程简介
在 Java 应用编程的上下文中,进程是操作系统中的执行单元。当你启动一个程序,你就启动了一个过程。当机器引导代码时,它做的第一件事就是执行引导过程。然后,此进程启动其他进程,这些进程将成为引导进程的子进程。这些子进程可能会启动其他进程。这样,当机器运行时,就会有进程树在运行。
当机器做某事时,它是在某个进程内执行的某个代码中完成的。操作系统还作为同时执行的多个进程运行。应用作为一个或多个进程执行。大多数应用都是作为一个进程运行的,但作为一个例子,Chrome 浏览器启动几个进程来执行所有渲染和网络通信操作,这些操作共同起到浏览器的作用
要更好地了解进程是什么,请启动 Windows 上的任务管理器或 OSX 上的活动监视器,然后单击“进程”选项卡。您将看到机器上当前存在的不同进程。使用这些工具,您可以查看进程的参数,并且可以逐个终止进程
单个进程为其工作分配了内存,不允许它们自由访问彼此的内存
操作系统调度的执行单元是线程。进程由一个或多个线程组成。这些线程由操作系统调度器调度,并在时隙中执行
对于每个操作系统,进程都有一个 PID,它是一个标识进程的数字。不能同时有两个进程共享同一 PID。当我们想要在操作系统中识别一个活动进程时,我们使用 PID。在 Linux 和其他类似 Unix 的操作系统上,kill命令终止进程。要传递给此程序的参数是要终止的进程的 PID。终止可以是优雅的。这有点像要求进程退出。如果进程决定不运行,它可以继续运行。
程序可以准备在收到此类请求时停止。例如,Java 应用可以添加一个调用Runtime.getRuntime().addShutdownHook(Thread t)方法的Thread对象。传递的线程应该在进程被要求停止时启动,这样线程就可以执行程序退出前必须执行的所有任务。不幸的是,不能保证线程会真正启动,这取决于实际的实现。
使用ProcessHandle接口
Java9 中引入了两个支持处理操作系统进程的新接口-ProcessHandle和ProcessHandle.Info。
ProcessHandle对象标识操作系统进程并提供管理该进程的方法。在以前的 Java 版本中,这只能通过特定于操作系统的方法使用 PID 来标识进程。这种方法的主要问题是,PID 只有在进程处于活动状态时才是唯一的。当一个进程完成时,操作系统可以自由地为一个新进程重用 PID。当我们使用 PID 检查一个进程是否仍在运行时,我们实际上是在用该 PID 检查一个活动进程。当我们检查进程时,它可能是活动的,但是下次程序查询进程状态时,它可能是另一个进程
桌面和服务器操作系统尽量不重用 PID 值。在某些嵌入式系统上,操作系统可能只使用 16 位值来存储 PID。当仅使用 16 位值时,PIDs 被重用的可能性更大。我们现在可以使用ProcessHandleAPI 来避免这个问题。我们可以接收ProcessHandle,也可以调用handle.is.Alive()方法。此方法将在进程完成时返回false。即使重用了 PID,这种方法也可以工作。
获取当前进程的 PID
我们可以通过handle访问进程的 PID。handle.getPid()方法返回Long表示 PID 的数值,由于通过句柄访问进程更安全,因此该方法的重要性受到限制。当我们的代码想要将自己的信息提供给其他管理工具时,它可能会派上用场。
程序通常会创建一个以数字 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());
}
}
以下是前面代码的控制台输出:

列出进程
在 Java9 之前,我们没有获得活动进程列表的方法。使用 Java9、10 和 11,可以在Stream中获取进程。有三种方法返回Stream<ProcessHandle>,用于:
- 列出子进程
- 列出所有子项
- 列出所有进程
下一节将对每一项进行回顾。
列出子项
为了得到控制子进程的进程句柄的Stream,应该使用静态方法processHandle.children()。这将创建processHandle表示的进程的子进程的快照,并创建Stream,由于进程是动态的,因此不能保证在代码执行过程中,当我们的程序处理句柄时,所有子进程仍然是活动的。它们中的一些可能会终止,而我们的进程可能会产生新的子进程,可能来自不同的线程。因此,代码不应该假设Stream的ProcessHandle元素代表一个活动的、正在运行的进程
以下程序在 Windows 中启动 10 个命令提示,然后计算子进程的数量并将其打印到标准输出:
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:
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 进程:
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 平台提供了一种更好的方法来处理等待过程,ProcessHandle接口有一个名为onExit()的方法返回CompletableFuture。这个类可以在不循环的情况下等待任务完成。如果我们有一个进程的句柄,我们可以简单地调用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并不意味着进程实际上已经终止。这只意味着操作系统接受了终止请求,并且操作系统将在将来的某个时候终止进程。这实际上很快就会发生,但不是瞬间发生的,因此如果方法isAlive()在destroy()或destroyForcefully()方法返回值true之后的一段时间内返回true,也就不足为奇了。
destroy()和destroyForcefully()之间的区别是具体实现的。Java 标准没有规定destroy()终止让关闭序列执行的进程。它只请求终止进程。此ProcessHandle对象表示的进程是否正常终止取决于实现。
这是因为某些操作系统没有实现优雅的进程终止特性。在这种情况下,destroy()的实现与调用destroyForcefully()相同。接口ProcessHandle的系统特定实现必须实现方法supportsNormalTermination(),只有当实现支持正常(非强制)进程终止时,才应该是true。对于实际实现中的所有调用,该方法应返回相同的值,并且在执行 JVM 实例期间不应更改返回值。不需要多次调用该方法。
下面的示例演示了进程启动、进程终止和等待进程终止。在我们的示例中,我们使用两个类。第一个类演示了.sleep()方法:
public class WaitForChildToBeTerminated {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(10_000);
}
}
我们示例中的第二个类称为WaitForChildToBeTerminated类:
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.java11.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,这些对象是使用各个进程的句柄创建的
当所有进程完成后,它以毫秒为单位打印出测量的时间。时间间隔从进程创建和进程创建循环完成时开始。当 JVM 从join()方法返回时,当进程被识别时,测量的时间间隔结束
示例代码将睡眠时间设置为 10 秒。这是一个更明显的时间段。运行两次代码并删除破坏进程的行会导致打印速度慢得多。实际上,测量和打印的运行时间也会显示终止进程会产生影响。
查看示例进程控制器应用
最后一节提供了一个示例过程控制应用来演示本章的内容。应用的功能非常简单。它从一系列配置文件参数中读取如何启动某些进程,然后,如果其中任何进程停止,它将尝试重新启动进程。
这个示例应用可以作为实际应用的起点。可以使用环境变量规范扩展进程的参数集。您还可以为进程、输入和输出重定向添加一个默认目录,甚至还可以添加一个进程的 CPU 消耗量,而无需控制应用终止并重新启动它
应用由四个类组成:
Main:此类包含public static void main方法,用于启动守护进程。Parameters:此类包含进程的配置参数。在这个简单的例子中,它只包含一个字段,即命令行。如果应用得到扩展,这个类将包含默认目录、重定向和 CPU 使用限制数据。ParamsAndHandle:这个类只不过是一个数据元组,其中包含对Parameters对象的引用,同时也是一个进程句柄。当一个进程死亡并重新启动时,进程句柄将被新的句柄替换,但是对Parameters对象的引用不会改变它的配置ControlDaemon:这个类实现了Runnable接口,作为一个单独的线程启动。
Main类
main()方法从命令行参数中获取目录名。它将此视为相对于当前工作目录。它使用同一类中的单独方法从目录中的文件读取配置集,然后启动控制守护进程。以下代码是程序的main()方法:
public static void main(String[] args) throws IOException,
InterruptedException {
// DemoOutput.out() simulated - implementation not 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 应该保持活动状态,直到运算符发出 Unixkill命令或在命令行上按Ctrl + C将其杀死。
使用 JDK 中新的Files和Paths类,获取指定目录中的文件列表并从文件中获取参数非常简单:
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对象,然后过滤出configuration目录中的目录,并使用静态方法将剩余的普通文件从Parameters类的File映射到Parameters对象。最后,我们返回对象的Set。
Parameters类
我们的Parameters类有一个字段和一个构造器,如下所示:
final String[] commandLine;
public Parameters(String[] commandLine) {
this.commandLine = commandLine;
}
Parameters类有两个方法。第一个方法getCommandLineStrings()从属性中检索命令行字符串。此数组包含命令和命令行参数。如果文件中没有定义,则返回一个空数组,如下所示:
private static String[] getCommandLineStrings(Properties props) {
return Optional.ofNullable(props.getProperty("commandLine"))
.orElse("").split("\\s+");
}
第二种方法是静态的fromFile(),它从properties文件中读取属性,如下所示:
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 () 方法就在那里,所以我们可以将它用作方法句柄,我们将在第 10 章、“细粒度栈跟踪”中看到。
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());
}
因为此时没有启动进程,所以句柄都是null。使用run()方法启动进程,如下所示:
@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()方法调用返回CompletableFuture的anyOfMyProcesses()方法,该方法在任何托管进程退出时完成。方法计划在完成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流,最后将其收集到列表并转换为数组。
我们完成示例应用的最后一种方法如下:
private ProcessHandle start(Parameters params)
throws IOException {
return new ProcessBuilder(params.commandLine)
.start().toHandle();
}
此方法使用ProcessBuilder启动进程并返回ProcessHandle,以便替换集合中的旧进程并管理新进程。
总结
在本章中,我们讨论了当前的 Java 平台如何使我们能够管理进程。这与早期版本的 Java 相比有了很大的改进,后者需要特定于操作系统的实现,而且在 CPU 使用和编码实践方面还不够理想。现代的 API,加上像ProcessHandle这样的新类,使得处理进程的几乎所有方面成为可能。我们还构建了一个完整的应用,管理将学到的 API 付诸实践的进程。
在下一章中,我们将详细介绍 Java 栈遍历 API。我们将使用代码示例来说明如何使用 API。
问题
-
什么是进程?
-
哪两个接口支持处理操作系统进程?
-
当一个进程结束时,什么方法返回
false? -
如何访问进程的 PID?
-
如何检索当前进程的 PID?
-
列出
Info接口用于传递进程信息的六种查询方法。 -
用什么方法得到控制子进程的进程句柄的
Stream? -
使用什么方法来获取子体的进程句柄的
Stream? -
什么方法可以用来检索所有子代和子代的列表?
-
onExit()方法返回什么?
进一步阅读
有关详细信息,请访问以下链接:
- 《Java9 高性能》在这个页面提供。
十、细粒度栈跟踪
在最后一章中,我们探讨了Process类和java.lang.ProcessHandleAPI。在 Java 早期版本中,Java 中的流程管理需要特定于 OS 的实现,在 CPU 使用和编码实践方面,它的实现不如最优。现代 API,带有诸如ProcessHandle等新类,使得几乎可以处理过程管理的所有方面。具体来说,我们介绍了过程,使用ProcessHandle接口,并回顾了一个示例过程控制器应用。
在本章中,我们将重点介绍 Java 的StackWalkerAPI。API 支持普通程序很少需要的特殊功能。API 在一些非常特殊的情况下非常有用,比如框架提供的功能。因此,如果您想要一种有效的栈遍历方法,使您能够对栈跟踪信息进行可过滤的访问,那么您将喜欢使用StackWalkerAPI。API 提供了对调用栈的快速优化访问,实现了对单个帧的延迟访问。
具体来说,我们将讨论以下主题:
- Java 栈概述
- 栈信息的重要性
- 使用
StackWalker StackFrame- 性能
技术要求
本章以及随后的章节以 Java18.9(也称为 Java11)为特色。Java 平台的标准版(SE)可以从 Oracle 的官方下载站点下载。
集成开发环境(IDE)包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从以下网站下载。
Java 栈概述
在深入研究StackWalker之前,让我们先介绍一下 Java 栈。我们将查看基本栈信息,而不是针对StackWalker。
Java 运行时有一个名为Stack的类,可以使用后进先出(LIFO)策略来存储对象。算术表达式是使用栈计算的。如果我们在代码中加上A和B,首先将A推送到操作数栈上,然后将B推送到操作数栈上,最后执行加法运算,取操作数栈最上面的两个元素并推送结果,A+B那里。
JVM 是用 C 编写的,并执行调用 C 函数并从那里返回。此调用返回序列使用本机方法栈与其他 C 程序一样进行维护。
最后,当 JVM 创建一个新线程时,它还会分配一个调用栈,其中包含一个帧,该帧依次包含本地变量、对上一个帧的引用以及对包含执行方法的类的引用。当调用一个方法时,会创建一个新的框架。当一个方法完成它的执行时,框架就被破坏了;换句话说,它返回或抛出一个异常。这个栈,Java 虚拟机栈,是StackWalkerAPI 管理的栈。
栈信息的重要性
一般来说,我们在开发依赖调用方的代码时需要栈信息。拥有关于调用者的信息可以让我们的代码根据这些信息做出决策。在一般实践中,让功能依赖于调用者不是一个好主意。影响方法行为的信息应该可以通过参数获得。依赖调用方的代码开发应该相当有限。
JDK 使用 Java 应用不可用的本机方法访问栈信息。SecurityManager类是定义应用安全策略的类。此类检查是否允许反射 API 的调用方访问另一个类的非公共成员。要做到这一点,它必须能够访问调用者类,并通过受保护的本机方法实现这一点。
这是一个实现一些安全措施而不必遍历栈的示例。我们为外部开发人员打开代码,将其用作库。我们还调用库用户提供的类的方法,它们可能会回调到我们的代码。我们希望允许库用户调用某些代码,但前提是这些代码不是从我们的代码中调用的。如果我们不想让库使用代码直接访问某些代码,我们可以使用 Java 的模块化结构,而不导出包含不被调用的类的包。这就是我们设置额外条件的原因,即代码对来自外部的调用者可用,除非它们是由我们的代码调用的:

隔离受保护代码
另一个例子是当我们想要访问一个记录器时。Java 应用使用许多不同的记录器,并且日志记录系统通常非常灵活,因此可以根据实际需要打开和关闭不同记录器的输出,以便对代码进行内省。最常见的做法是为每个类使用不同的记录器,记录器的名称通常是类的名称。这种做法非常普遍,日志框架甚至提供了记录器访问方法,这些方法接受对类本身的引用而不是名称。它本质上意味着获取记录器句柄的调用如下所示:
private static final Logger LOG = Logger.getLogger(MyClass.class);
如果在获取新记录器的调用中忘记更改类名的名称,则在从现有类创建新类时可能会出现问题。这不是一个严重的问题,但它是常见的。在这种情况下,我们的代码将使用另一个类的记录器,它实际上可以工作,但在分析日志文件时可能会造成混乱。如果我们有一个方法返回名为调用方类的记录器,那就更好了。
让我们在接下来的两节中用示例代码片段继续探索栈信息。
示例-限制调用者
在本节中,我们将用两种方法开发一个示例库。hello()方法将hello打印到标准输出。callMe()方法接受Runnable作为参数并运行它。然而,第一种方法受到限制。它只在调用方完全在库之外时执行。如果调用方以调用库的方式获得控件,则抛出IllegalCallerException,可能是通过调用传递的Runnable的第二个方法。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()方法调用的方法。此方法创建StackWalker并调用walk()方法。walk()方法的参数是一个函数,它将StackFrame对象的Stream转换为walk()方法将返回的其他值。
一开始,这个参数设置似乎很复杂,很难理解。更合乎逻辑的做法是返回提供StackFrame对象的Stream,而不是强制调用者定义一个将其作为参数的函数。
示例代码使用 Lambda 表达式将函数定义为walk()方法的参数。Lambda 表达式的参数是流。因为这个流的第一个元素是实际的调用,所以我们放弃它。因为如果调用方不符合条件,也应该拒绝这些调用,即使对hello()方法的调用是通过库中已经存在的其他类和方法进行的,所以我们从框架中删除属于CheckEligibility类包中类的所有元素。这个包是packt.java9.deep.stackwalker.myrestrictivelibrary,在代码中,这个字符串存储在packageName字段中。结果流只包含来自库外部的StackFrame对象。我们把这些也扔下去,直到流耗尽,或者直到我们发现StackFrame又属于库。如果所有的元素都消失了,我们就好了。在这种情况下,count()的结果为零。如果我们在StackFrame中找到一个属于库的类,这意味着外部代码是从库中调用的,在这种情况下,我们必须拒绝工作。在这种情况下,变量eligible将是false,我们抛出一个异常,如下面的屏幕截图所示:

StackFrame在库中找到的类
示例–为调用者获取记录器
在 Java 中,我们使用 API 来获取Logger。使用 API,模块可以为服务LoggerFinder提供实现,服务LoggerFinder可以返回实现getLogger()方法的Logger。这消除了库对特定记录器或记录器外观的依赖,这是一个巨大的优势。还有一个更小但仍然很烦人的问题需要我们在getLogger()方法的参数中再次写入类名。
为了避免这个繁琐的任务,我们创建了一个辅助类来查找调用者类并检索适合调用者类和模块的记录器。因为在这种情况下不需要栈跟踪中引用的所有类,所以我们将调用StackWalker类的getCallerClass()方法。我们在packt.java9.deep.stackwalker.logretrieve包中创建一个名为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());
}
}
在 Java9 之前,这个问题的解决方案是从Thread类中获取StackTrace数组,并从中查找调用者类的名称。另一种方法是扩展SecurityManager,它有一个受保护的方法getClassContext(),该方法返回栈上所有类的数组。这两种解决方案都遍历栈并组成一个数组,尽管我们只需要数组中的一个元素。在Logger检索的情况下,这可能不是显著的性能损失,因为记录器通常存储在private static final字段中,因此在类初始化期间每个类初始化一次。在其他用例中,性能损失可能很大。
接下来,我们来看看StackWalker的细节。
与StackWalker合作
在本节中,您将熟悉如何使用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类中的StackWalker.Option枚举,有三个值:
RETAIN_CLASS_REFERENCESHOW_REFLECT_FRAMESSHOW_HIDDEN_FRAMES
枚举选项
RETAIN_CLASS_REFERENCE、SHOW_REFLECT_FRAMES和SHOW_HIDDEN_FRAMES枚举选项具有自描述性名称,下面将对其进行说明。
RETAIN_CLASS_REFERENCE
如果我们指定第一个选项的枚举常量,RETAIN_CLASS_REFERENCE作为getInstance()方法的参数,那么返回的实例将授予我们访问各个栈在遍历期间引用的类的权限。
SHOW_REFLECT_FRAMES
SHOW_REFLECT_FRAMES枚举常量将生成一个遍历器,其中包含来自某个反射调用的帧。
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
.getInstance(SHOW_HIDDEN_FRAMES)
.forEach(System.out::println);
}
前面三种方法将帧打印到标准输出。他们使用StackWalker的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(),后者依次调用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(NativeMethod)
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()方法,该方法在不同的类中定义,该方法反过来调用invoke()方法,该方法是 JVM 提供的本机方法。然后,我们终于找到了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(NativeMethod)
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_FRAMES和SHOW_HIDDEN_FRAMES组合为一个选项集,但这样做并没有什么好处。后者包括前者,因此两者的结合与后者完全相同。
访问类
当我们想在栈遍历期间访问类对象时,我们必须指定RETAIN_CLASS_REFERENCE选项。虽然StackFrame接口定义了getClassName()方法,但可以使用Class.forName()方法访问名称为的类,这样做并不能保证StackFrame对象引用的类是由调用Class.forName()的代码所在的类装入器装入的。在某些特殊情况下,我们可能会得到由两个不同的类装入器装入的两个同名的不同类。
如果在创建StackWalker实例的过程中没有使用该选项,则返回类对象的方法将抛出UnsupportedOperationException异常。这样,getDeclaringClass()就不能用在StackFrame上,getCallerClass()不能用在StackWalker上。
walk()方法
StackWalker类定义了forEach()方法,该方法期望Consumer(最好是以 Lambda 表达式的形式),该方法为向上遍历栈的栈跟踪的每个元素调用。Consumer方法的参数是StackFrame对象。
尽管名为forEach的方法也是由Stream接口定义的,并且walk()方法将Stream对象作为参数传递给它得到的函数,但我们不应混淆这两者。StackWalker中的forEach()方法是一种更简单的方法,大多数情况下是一种不太有效的方法,可以穿透栈跟踪的所有元素。
在大多数情况下,它的效率较低,因为它强制StackWalker实例获取栈跟踪的所有元素,这样forEach()方法就可以遍历每个元素到最后。如果我们知道我们不会遍历栈跟踪到最后,我们应该使用walk()方法,即以惰性的方式访问栈,从而为性能优化留下更多的空间。
StackWalker类有walk()方法,这是使其成为遍历器的定义方法。该方法接受由StackWalker调用的函数。walk()方法的返回值将是函数返回的对象。函数的参数是传递栈帧的Stream<StackFrame>。第一帧是包含walk()方法调用的帧,下一帧是调用包含walk()方法调用的帧,依此类推。
该函数可用于根据来自流的StackFrame对象计算一些值,并决定调用方是否有资格调用我们的代码。
在回顾了walk()方法之后,您可能会想,这个方法需要一个函数,而函数又得到一个Stream<StackFrame>作为参数,为什么它如此复杂。理想情况下,我们可以直接从StackWalter实例获取Stream<StackFrame>。最简单的方法是将流从函数传回。考虑以下示例:
// EXAMPLE OF WHAT NOT TO DO!!!!
public static void itIsNotCallBack() {
Stream<StackWalker.StackFrame> stream = StackWalker
.getInstance(RETAIN_CLASS_REFERENCE)
.walk(s -> s);
// The following results in an EXCEPTION
boolean eligible = stream
.dropWhile(CheckEligibility::inLibrary)
.dropWhile(CheckEligibility::notInLibrary)
.count() == 0;
if (!eligible) {
throw new IllegalCallerException();
}
}
我们所做的只是直接从遍历器调用返回流,然后遍历流,然后执行相同的计算。我们的结果是IllegalStateException异常,而不是资格检查。
原因是StackWalker的实现高度优化。它不会复制整个栈来为流提供源信息。它是从实际的,活生生的栈中工作的。为此,必须确保在使用流时不修改栈。这与迭代集合时更改集合可能得到的ConcurrentModificationException异常非常相似。如果我们在调用栈中向上传递流,然后想要从中获取StackFrame,那么流将尝试从早已消失的栈帧中获取信息,因为我们从它所属的方法返回。这样,StackWalker就不会生成整个栈的快照,而是从实际栈开始工作,并且必须确保所需的栈部分不会更改。我们可以从函数中调用方法,这样我们可以在调用链中更深入地挖掘,但是在流被使用时,我们不能得到更高的值。
StackWalker类是final类,不能扩展。
StackFrame
在前面的部分中,我们遍历了StackFrame元素并提供了示例代码片段。接下来,我们将更仔细地研究它。StackFrame是StackWalker类内部定义的接口。它定义了访问器,是一个转换器,可用于将信息转换为StackTraceElement。
接口定义的访问器如下:
getClassName()返回StackFrame表示的方法类的二进制名称。getMethodName()返回StackFrame表示的方法名称。getDeclaringClass()返回StackFrame表示的方法的类。如果在创建StackWalker实例时没有使用Option.RETAIN_CLASS_REFERENCE,则该方法将抛出UnsupportedOperationException。getByteCodeIndex()获取包含StackFrame表示的方法的执行点的代码数组的索引。当查看命令行工具javap可以提供给我们的反汇编 Java 代码时,这个值的使用在 bug 搜索期间会很有帮助。这个值的编程使用只能对直接访问代码的字节码的应用、Java 代理或在运行时生成字节码的库有价值。如果方法是本机的,则该方法将返回一个负数。getFileName()返回定义了StackFrame表示的方法的源文件名。getLineNumber()返回源代码的行号。- 如果
StackFrame表示的方法是本机方法,isNativeMethod()返回true,否则返回false。
StackFrame不提供任何方法来访问该方法所属的对象。您无法访问由StackFrame表示的方法的参数和局部变量,并且没有其他方法可以实现这一点。这很重要。这样的访问太具侵入性,是不可能的。
性能
如果不考虑性能因素,我们对StackWalker的报道是不完整的。
StackWalker高度优化,不会产生大量未使用的内存结构。这就是为什么我们必须使用传递给walker()方法的函数作为参数的原因。这也是创建时,StackTrace不会自动转换为StackTraceElement的原因。只有当我们查询方法名,即特定的StackTraceElement的行号时,才会发生这种情况。理解这种转换需要花费大量的时间是很重要的,如果它在代码中用于某种调试目的,则不应将其留在那里。
为了使StackWalker更快,我们可以提供我们将在流中工作的StackFrame元素的估计数。如果我们不提供这样的估计,JDK 中的当前实现将使用八个预分配的StackFrame对象,当这些对象用完时,JDK 将分配更多的对象。JDK 将根据我们的估计分配元素的数量,除非我们估计的值大于 256。在这种情况下,JDK 将使用 256。
总结
在本章中,我们学习了如何使用StackWalkerAPI,并检查了示例代码片段以加强我们的理解。我们对 API 的详细审查包括不同的使用场景、选项和信息。我们探讨了 API 的复杂性,并分享了如何使用和如何不使用该类。最后我们讨论了一些开发人员应该注意的相关性能问题。
在下一章中,我们将介绍与现代 Java 平台相关的十几种工具和工具增强。这些特色的变化将涵盖各种各样的工具和 API 的更新,这些工具和 API 旨在使使用 Java 进行开发变得更容易,并且能够创建优化的 Java 应用。我们将介绍新的 HTTP 客户端、对 Javadoc 和 Doclet API 的更改、新的 JavaScript 解析器、JAR 和 JRE 更改、新的 Java 级 JVM 编译器接口、对 TIFF 图像的支持、平台日志记录、XML 目录支持、集合、新的特定于平台的桌面功能、对方法处理的增强以及废弃注解。
问题
- Java 如何使用栈存储对象?
- Java 对调用返回序列使用什么栈?
StackWalkerAPI 管理什么栈?- 如何检索记录器?
getCallerClass()方法属于哪一类?StackWalker.Option枚举的可能值是什么?RETAIN_CLASS_REFERNCE枚举用于什么?SHOW_REFLECT_FRAMES枚举用于什么?SHOW_HIDDEN_FRAMES枚举用于什么?StackWalker类怎么扩展?
十一、新工具和工具增强功能
在上一章中,我们学习了如何使用StackWalkerAPI,并检查了示例代码片段以加强我们的理解。我们对 API 的详细审查包括不同的使用场景、选项和信息。我们探讨了 API 的复杂性和类用法的共享细节。在本章的结尾,我们介绍了与StackWalkerAPI 相关的性能问题。
在本章中,我们将介绍十几种与现代 Java 平台相关的工具和工具增强。这些特色的变化将涵盖广泛的工具和 API 的更新,这些工具和 API 旨在简化 Java 开发,增强创建优化 Java 应用的能力。
更具体地说,我们将审查以下主题:
- HTTP 客户端
- Javadoc 和 Doclet API
- mJRE 变更
- JavaScript 解析器
- 多版本 JAR 文件
- Java 级 JVM 编译器接口
- TIFF 支持
- 平台日志记录
- XML 目录
- 集合
- 特定于平台的桌面功能
- 增强的方法句柄
- 废弃的改进
- 本机头生成工具(
javah)
技术要求
本章介绍 Java11,Java 平台的标准版(SE)可从 Oracle 官方下载网站下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
本章的源代码可以在 GitHub 的 URL 上找到。
使用 HTTP 客户端
在本节中,我们将回顾 Java 的超文本传输协议(HTTP)客户端,从旧的 Java9 之前的客户端开始,然后深入到作为当前 Java 平台一部分的新 HTTP 客户端。最后,我们将看看当前 HTTP 客户端的局限性。需要这种方法来支持对变化的理解。
Java9 之前的 HTTP 客户端
JDK1.1 版引入了支持 HTTP 特定特性的HttpURLConnectionAPI。这是一个健壮的类,包含以下字段:
-
chunkLength -
fixedContentLength -
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_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_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 平台进行更新。这些问题如下:
- 基本的
URLConnectionAPI 已经失效了一些协议,比如 Gopher 和 FTP,这些年来成为了一个日益严重的问题 HttpURLConnectionAPI 早于 HTTP1.1,过于抽象,使其不易使用- HTTP 客户端的文档记录严重不足,使得 API 令人沮丧,难以使用
- 客户端一次只能在一个线程上运行
- 由于 API 早于 HTTP1.1,并且缺乏足够的文档,因此维护起来非常困难
现在我们知道以前的 HTTP 客户端有什么问题了,让我们看看当前的 HTTP 客户端。
Java11 HTTP 客户端
为现代 Java 平台创建新的 HTTP 客户端有几个相关的目标,java9、10 和 11 提供了这些目标。下表列出了主要目标。这些目标分为易用性、核心功能、附加功能和性能等大类:
| 易用性 | API 旨在提供高达 90% 的 HTTP 相关应用要求。 |
|---|---|
| 对于最常见的用例,新 API 是可用的,没有不必要的复杂性。 | |
| 包括一个简单的阻塞模式。 | |
| API 支持现代 Java 语言功能。 Lambda 表达式是一个与 Java 8 一起发布的主要新介绍,就是一个例子。 | |
| 核心能力 | 支持 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 的内存消耗。 | |
| 避免运行计时器线程。 |
HTTP 客户端 API 的限制
HTTP 客户端 API 有一些故意的缺点。虽然这听起来可能有悖常理,但新的 API 并不打算完全取代当前的HttpURLConnectionAPI。相反,新的 API 最终将取代当前的 API。
下面的代码片段提供了如何实现HttpURLConnect类以在 Java 应用中打开和读取 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 不包括可插入的连接缓存。
- 缺乏通用的升级机制。
了解 Javadoc 和 Doclet API
Javadoc 和 Doclet API 密切相关。Javadoc 是一个文档工具,DocletAPI 提供了一些功能,以便我们可以检查嵌入在库和程序源代码级别的 Javadoc 注释。在本节中,我们将回顾 DocletAPI(Java9 之前)的早期状态,然后探讨在当前 Java 平台中引入 DocletAPI 的更改。最后,我们将回顾 Javadoc。
Java9 之前的 Doclet API
Java9 DocletAPI 之前的版本,或者com.sun.javadoc包,使我们能够查看源代码中的 Javadoc 注释。调用 Doclet 是通过使用start方法完成的。此方法的签名为public static boolean start(RootDoc root)。我们将使用RootDoc实例作为程序结构信息的容器。
为了调用 Javadoc,我们需要传递以下信息:
- 包名称
- 源文件名(用于类和接口)
- 访问控制选项可以是以下选项之一:
packageprivateprotectedpublic
当前面列出的项目用于调用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 refers 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
// 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_1和Java_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 -
ConstructorDoc -
DoCErrorReporter -
ExecutableMemberDoc -
FieldDoc -
MemberDoc -
MethodDoc -
PackageDoc -
ParameterizedType -
ProgramElementDoc -
RootDoc -
SeeTag -
SerialFieldTag -
SourcePosition -
Tag -
Type -
WildcardType
现有 Doclet API 的问题
先前存在的 Doclet API 存在几个问题,这些问题增加了对新 Doclet API 的需求:
- 它不适合于测试或并发使用。这源于它对静态方法的实现。
- API 中使用的语言模型有几个限制,并且随着每次 Java 升级而变得更麻烦。
- API 效率低下,主要是因为它大量使用子字符串匹配。
- 没有提及任何注释的具体位置。这使得诊断和故障排除变得困难。
Java9 的 Doclet API
既然您已经很好地掌握了 Java9 之前存在的 Doclet API,那么让我们看看 Java9 平台已经做了哪些更改并交付了哪些更改。新的 Doclet API 在jdk.javadoc.doclet包中。
在较高级别上,Doclet API 的更改如下:
- 更新
com.sun.javadocDoclet API 以利用几个 JavaSE 和 JDKapi - 更新
com.sun.tools.doclets.standard.StandardDoclet 以使用新的 API - 支持用于创建自定义
javadoc标记的更新的 Taglet API
除上述更改外,新 API 还使用以下两个 API:
- 编译器树 API
- 语言模型 API
让我们在下面的部分中探讨每一个问题。
编译器树 API
编译树 API 在com.sun.source.doctree包中。它提供了几个接口来记录源代码级别的注释。这些 API 表示为抽象语法树(AST)。
有两个枚举,如下所示:
-
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内联标签 |
不适用 |
DocTree |
不适用 | 所有用户的通用接口 | accept(DocTreeVisitor<R,D>visitor,Ddata), getKind() |
DocTreeVisitor<R,P> |
药方: | 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内联标签 |
不适用 |
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 组件中的每一个。
AnnotatedConstruction接口
AnnotatedConstruction接口为语言模型 API 提供了一个可注解的构造,该 API 自版本 1.8 以来一直是 Java 平台的一部分。适用于元素(接口Element)或类型(接口TypeMirror)的构造,每个构造的注解不同,如下表所示:
| 构造类型 | 接口 | 注释 |
|---|---|---|
element |
Element |
宣言 |
type |
TypeMirror |
基于类型名的使用 |
AnnotatedConstruction接口有三种方式:
getAnnotation(Class<A> annotationType):返回构造的注解类型getAnnotationMirrors():此方法返回构造上的注解列表getAnnotationsByType(Class<A> annotationType):此方法返回构造的相关注解
SourceVersion枚举
SourceVersion枚举由以下常量组成:
RELEASE_0RELEASE_1RELEASE_2RELEASE_3RELEASE_4RELEASE_5RELEASE_6RELEASE_7RELEASE_8RELEASE_9
预计随着 Java 平台的正式发布,SourceVersion枚举将更新为包含RELEASE_10和RELEASE_11。
此枚举还包含以下几种方法:
方法名称:isIdentifier:
public static boolean isIdentifier(CharSequence name)
如果参数字符串是 Java 标识符或关键字,则返回true。
方法名称:isKeyword:
public static boolean isKeyword(CharSequence s)
如果给定的CharSequence是文本或关键字,则此方法返回true。
方法名称:isName:
public static boolean isName(CharSequence name)
如果CharSequence是有效名称,则返回true。
方法名称:latest:
public static SourceVersion latest()
此方法返回用于建模的最新源版本。
方法名称:latestSupported:
public static SourceVersion latestSupported()
此方法返回可完全支持建模的最新源代码版本。
方法名称:valueOf:
public static SourceVersion valueOf(String name)
此方法基于提供的参数字符串返回枚举常量。
您应该知道,value(String name)方法抛出两个异常:IllegalArgumentException和NullPointerException。
方法名称:values:
public static SourceVersion[] values()
此方法返回枚举常量的数组。
UnknownEntityException
UnknownEntityException类扩展了RuntimeException,是未知异常的超类。类构造器如下所示:
protected UnknownEntityException(String message)
构造器使用作为字符串参数提供的消息创建一个新的UnknownEntityException实例。该方法不接受其他参数。
这个类没有自己的方法,但是从java.lang.Throwable和class.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
Javadoc 工具已针对现代 Java 平台(定义为 Java9 及更高版本)进行了更新。除了 HTML4 之外,它还可以生成 HTML5 标记输出。Javadoc 工具提供了对 HTML4 和 HTML5 的支持。从 Java10 开始,HTML5 是默认的输出标记格式。
下面的简短 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 IDEA 中,选择“工具”下拉菜单,然后选择“生成 JavaDoc”。
下面的截图显示了生成 Javadoc 功能的 IntelliJ IDEA 接口。如您所见,-html5命令行参数已包含:

生成 Javadoc
单击“确定”按钮时,您将看到一系列状态消息,如以下示例所示:
"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\JDOutput
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 时,创建了多个文档,如以下屏幕截图中提供的目录树所示:

Javadoc 目录树
您还可以添加 Javadoc 工具识别的可选标记。此处提供了这些标签:
@author@code@deprecated@docRoot@exception@inheritDoc@link@linkplain@param@return@see@serial@serialData@serialField@since@throws@value@version
有关如何为 Javadoc 工具编写文档注释的更多信息,请访问 Oracle 的官方说明。
Javadoc 搜索
在 Java9 之前,标准 Doclet 生成的 API 文档页面很难导航。除非您非常熟悉这些文档页面的布局,否则您可能会使用基于浏览器的查找功能来搜索文本,这被认为是笨拙和次优的。
当前平台包括一个搜索框作为 API 文档的一部分。此搜索框由标准 Doclet 授予,可用于搜索文档中的文本。这为开发人员提供了极大的便利,可能会改变我们对 Doclet 生成的文档的使用。
通过新的 Javadoc 搜索功能,我们可以搜索以下索引组件:
- 模块名称
- 包名称
- 类型
- 成员
- 使用新的
@index内联标签索引的术语/短语
大小写搜索
Javadoc 搜索功能通过使用驼峰大小写搜索提供了一个很好的快捷方式。例如,我们可以搜索openED来找到openExternalDatabase()方法。
对多重 JRE 功能的更改
mJRE(简称多重 JRE)特性以前用于指定启动应用的特定 JRE 版本或版本范围。我们可以通过命令行选项-version或者通过 JAR 文件清单中的一个条目来实现这一点。以下流程图说明了根据我们的选择所发生的情况:

多 JRE 流
这个功能是在 JDK5 中引入的,在该版本或 JDK9 之前的任何后续版本中都没有完整的文档记录。
现代平台引入了以下具体变化:
- 已删除 mJRE 功能。
- 现在,只要使用
-version命令行选项,启动器就会产生错误。这是一个终端错误,处理将无法继续。 - 在 Java9 中,如果 JAR 的清单中有一个
-version条目,就会产生一个警告。警告不会停止执行。 - 在 Java10 和 Java11 中,清单文件中存在一个
-version条目将导致终端错误。
JavaScript 解析器
Java 平台最近的一个变化是为 Nashorn 的 ECMAScript AST 创建了一个 API。在本节中,我们将分别介绍 Nashorn、ECMAScript,然后介绍解析器 API。
Nashorn
Oracle Nashorn 是 Oracle 用 Java 开发的 JVM 的 JavaScript 引擎。它是与 Java8 一起发布的,旨在为开发人员提供一个高效、轻量级的 JavaScript 运行时引擎。使用这个引擎,开发人员能够在 Java 应用中嵌入 JavaScript 代码。在 Java8 之前,开发人员可以访问 Netscape 创建的 JavaScript 引擎。该引擎于 1997 年推出,由 Mozilla 维护。
Nashorn 既可以用作命令行工具,也可以用作 Java 应用中的嵌入式解释器。让我们看看这两个例子
Nashorn 是德语中犀牛的意思。这个名字来源于 Mozilla 基金会的 Rhino JavaScript 引擎。据说犀牛起源于一本书封面上的动物图片。把这个放在有趣的事实下面。
使用 Nashorn 作为命令行工具
Nashorn 可执行文件jjs.exe位于\bin文件夹中。要访问它,您可以导航到该文件夹,或者,如果您的系统路径设置正确,您可以通过在系统的终端/命令提示符窗口中输入jjs命令来启动 Shell:

Nashorn 可执行文件的位置
在这里,您可以看到一个打开的终端窗口,它首先检查 Java 的版本,然后使用jjs -version命令启动 Nashorn shell。在本例中,Java 和 Nashorn 的版本都是 1.8.0.121。或者,我们可以简单地用jjs命令启动 Nashorn,Shell 将在没有版本标识的情况下打开:

用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 addtest.js命令。下面是输出:

用 Java 运行 JavaScript
你可以对 Nashorn 做很多事。在终端/命令提示符窗口中,我们可以使用-help选项执行jjs,以查看命令行命令的完整列表:

-help组件
如您所见,使用-scripting选项使我们能够通过使用 Nashorn 作为文本编辑器来创建脚本。使用 Nashorn 时,有几个内置函数非常有用:
echo():类似于System.out.print()Java 方法exit():这是 Nashorn 的出口load():从给定路径或 URL 加载脚本print():类似于System.out.print()Java 方法readFull():读取文件的内容readLine():读取stdin中的一行quit():这是 Nashorn 的出口
使用 Nashorn 作为嵌入式解释器
与将 Nashorn 用作命令行工具相比,Nashorn 更常用的用法是将其用作嵌入式解释器。javax.scriptAPI 是公共的,可以通过nashorn标识符访问。下面的代码演示了如何在 Java 应用中访问 Nashorn、定义 JavaScript 函数和获取结果:
// 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 的可能性。甲骨文的官方文档中有大量的例子。
ECMAScript
ECMA(简称欧洲计算机制造商协会)成立于 1961 年,是一个信息系统和通信系统的标准组织。今天,ECMA 继续制定标准并发布技术报告,以帮助标准化消费电子、信息系统和通信技术的使用方式。ECMA 有 400 多项标准,其中大部分已被采用。
你会注意到 ECMA 并不是用所有的大写字母拼写的,因为它不再被认为是首字母缩写。1994 年欧洲计算机制造商协会正式更名为 ECMA。
ECMAScript(也称为 ES)创建于 1997 年,是一种脚本语言规范。JavaScript 实现了此规范,包括以下内容:
- 补充技术
- 库
- 脚本语言语法
- 语义
分析器 API
Java 平台最近的一个变化是为 Nashorn 的 ECMAScript 抽象语法树提供了特定的支持。新 API 的目标是提供以下内容:
- 表示 Nashorn 语法树节点的接口
- 创建可以用命令行选项配置的解析器实例的能力
- 用于与 AST 节点接口的访问者模式 API
- 使用 API 的测试程序
新的 APIjdk.nashorn.api.tree是为了允许将来对 Nashorn 类进行更改而创建的。在新的解析器 API 之前,IDEs 使用 Nashorn 的内部 AST 表示进行代码分析。根据 Oracle 的说法,jdk.nashorn.internal.ir包的使用阻止了 Nashorn 内部类的现代化。
下面是新的jdk.nashorn.api.tree包的类层次结构:

jdk.nashorn.api.tree类层次结构
下图说明了新 API 的复杂性,具有完整的接口层次结构:

Nashorn 接口层次结构
jdk.nashorn.api.tree包的最后一个组件是枚举层次结构,如下所示:

枚举层次结构
多版本 JAR 文件
JAR 文件格式已经在 Java 平台中进行了扩展,现在允许在一个 JAR 文件中存在多个版本的类文件。类版本可以特定于 Java 发布版本。这种增强允许开发人员使用一个 JAR 文件来存放多个版本的软件
JAR 文件增强包括以下内容:
- 支持
JarFileAPI - 支持标准类装入器
对 JAR 文件格式的更改导致了对核心 Java 工具的必要更改,以便它们能够解释新的多版本 JAR 文件。这些核心工具包括:
javacjavapjdeps
最后,新的 JAR 文件格式支持模块化,这是现代 Java 平台的关键特性。对 JAR 文件格式的更改并没有导致相关工具或进程的性能降低。
识别多版本 JAR 文件
多版本 JAR 文件将有一个新属性,Multi-Release: true。该属性将位于 JARMANIFEST.MF主节中
标准 JAR 文件和多版本 JAR 文件的目录结构不同。下面是一个典型的 JAR 文件结构:

Javadoc 目录树
下图显示了新的多版本 JAR 文件结构,其中包含 Java8 和 Java9 的特定于 Java 版本的类文件:

JAR 文件结构
相关 JDK 更改
为了支持新的多版本 JAR 文件格式,必须对 JDK 进行一些更改。这些变化包括:
-
URLClassLoader是基于 JAR 的,经过修改,可以从指定的版本中读取类文件。 -
新的基于模块的类加载器是 Java9 的新成员,它可以从指定的版本读取类文件。
-
修改了
java.util.jar.JarFile类,以便从多版本 JAR 文件中选择适当的类版本。 -
jarURL 方案的协议处理器被修改,以便它从多版本 JAR 文件中选择适当的类版本。
-
Java 编译器
javac是用来读取类文件的已识别版本的,这些版本识别是通过使用JavacFileManagerAPI 和ZipFileSystemAPI 的-target和-release命令行选项来完成的。 -
为了利用
JavacFileManagerAPI 和ZipFileSystemAPI 的变化,对以下工具进行了修改: -
javah:生成 C 头文件和源文件 -
schemagen:这是 Java 类中名称空间的模式生成器 -
wsgen:这是 Web 服务部署的解析器 -
更新了
javap工具以支持新的版本控制模式。 -
修改了
jdeps工具以支持新的版本控制模式。 -
JAR 打包工具集也相应地更新了。该工具集由
pack200和unpack200组成。 -
当然,JAR 工具得到了增强,因此可以创建多版本 JAR 文件。
所有相关文档都已更新,以支持建立和支持新的多版本 JAR 文件格式所涉及的所有更改。
Java 级 JVM 编译器接口
基于 Java 的 JVM 编译器接口(JVMCI)允许 Java 编译器(必须是用 Java 编写的)被 JVM 用作动态编译器。
JVMCI 需求背后的原因是,它将是一个高度优化的编译器,不需要低级语言特性。一些 JVM 子系统需要低级功能,比如垃圾收集和字节码解释。所以,JVMCI 是用 Java 编写的,而不是用 C 或 C++ 编写的。这提供了 Java 一些最强大功能的附带好处,例如:
- 异常处理
- 既免费又健壮的 IDE
- 内存管理
- 运行时扩展性
- 同步
- 单元测试支持
由于 JVMCI 是用 Java 编写的,因此可以说维护起来更容易。
JVMCI API 有三个主要组件:
- 虚拟机数据结构访问
- 安装已编译代码及其元数据
- 使用 JVM 的编译系统
JVMCI 实际上在某种程度上存在于 Java8 中。JVMCIAPI 只能通过在引导类路径上处理代码的类加载器进行访问。在 Java9 中,这种情况发生了变化。它在当前的 Java 平台上仍然是实验性的,但是更容易访问。为了启用 JVMCI,必须使用以下一系列命令行选项:
-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler -Djvmci.Compiler=<name of compiler>
Oracle 将 JVMCI 保持在 Java9 中的实验性,以允许进一步的测试,并为开发人员提供最高级别的保护。
BeanInfo注解
@beaninfoJavadoc 标签已经被更合适的注解所取代。此外,这些新注解现在在运行时被处理,这样就可以动态生成BeanInfo类。Java 的模块化导致了这种变化。自定义BeanInfo类的创建已经简化,客户端库已经模块化。
为了充分把握这一变化,我们将在进一步讨论本 JEP 之前回顾JavaBean、BeanProperty和SwingContainer。
JavaBean
JavaBean是一个 Java 类。与其他 Java 类一样,JavaBean是可重用代码。它们在设计上是独特的,因为它们将多个对象封装成一个对象。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 注解,从 Java9 开始
BeanProperty注解具有以下可选元素:
boolean boundString descriptionString[] enumerationValuesboolean expertboolean hiddenboolean preferredboolean requiredboolean visualUpdate
SwingContainer
SwingContainer是注解类型。我们使用这个注解来指定与 Swing 相关的属性,这样我们就可以自动生成BeanInfo类。
SwingContainer注解具有以下可选元素:
String delegateboolean value
现在我们已经复习了JavaBean、BeanProperty和SwingContainer,让我们来看看BeanInfo类。
BeanInfo类
在大多数情况下,BeanInfo类是在运行时自动生成的。例外是Swing类。这些类基于@beaninfoJavadoc 标记生成BeanInfo类。这是在编译时完成的,而不是在运行时。从 Java9 开始,@beaninfo标记被@interface JavaBean、@interface BeanProperty和@interface SwingContainer注解所取代。
这些新注解用于根据前面部分中提到的可选元素设置相应的属性。例如,下面的代码片段设置了SwingContainer的属性:
package javax.swing;
public @interface SwingContainer {
boolean value() default false;
String delegate() default "";
}
这为我们提供了三个好处:
- 在
Bean类中指定属性要容易得多,而不必创建单独的BeanInfo类 - 我们将能够删除自动生成的类
- 使用这种方法,客户端库更容易模块化
TIFF 支持
图像输入/输出插件已经为现代 Java 平台进行了扩展,包括对 TIFF 图像格式的支持。ImageIO类扩展了Object类,是 JavaSE 的一部分。这个类包含几种编码和解码图像的方法。以下是静态方法列表:
| 方法 | 返回值 |
|---|---|
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) |
BufferedImage与ImageReader |
read(ImageInputStream stream) |
带ImageInputStream和ImageReader的BufferedImage |
read(InputStream input) |
带InputStream和ImageReader的BufferedImage |
read(URL input) |
BufferedImage与ImageReader |
还有一些静态方法不返回值或布尔值:
| 方法 | 说明 |
|---|---|
scanForPlugins() |
执行以下操作: |
| 扫描应用类路径以查找插件 | |
| 加载插件服务供应器类 | |
| 在 IORegistry 中注册服务供应器实例 | |
setCacheDirectory(File cacheDirectory) |
这是缓存文件的存储位置。 |
setUseCache(boolean useCache) |
此方法切换缓存是否基于磁盘。这适用于ImageInputStream和ImageOutputStream实例。 |
write(RenderedImage im, String formatName, File output) |
将图像写入指定的文件。 |
write(RenderedImage im, String formatName, ImageOutputStream output) |
将图像写入ImageOutputStream。 |
write(RenderedImage im, String formatName, OutputStream output) |
将图像写入OutputStream。 |
从提供的方法中可以看出,图像输入/输出框架为我们提供了使用图像编解码器的方便方法。从 Java7 开始,javax.imageio实现了以下图像格式插件:
- BMP
- GIF
- JPEG
- PNG
- WBMP
如您所见,TIFF 不在图像文件格式列表中。TIFF 是一种常见的文件格式,2001 年,MacOS 随着 MacOSX 的发布,广泛使用了这种格式
当前的 Java 平台包括用于 TIFF 的ImageReader和ImageWriter插件。这些插件是用 Java 编写的,并被捆绑在新的javax.imageio.plugins.tiff包中。
平台日志记录
现代 Java 平台包括一个日志 API,它使平台类能够记录消息,并提供相应的服务来操作日志。在我们深入了解日志 API 和服务的新特性之前,让我们回顾一下 Java7 中引入的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 平台中发生了哪些变化。
现代 Java 平台的日志
在 Java9 之前,有多种日志模式可用,包括java.util.logging、SLF4J和Log4J。后两种是第三方框架,它们有单独的外观和实现组件。这些模式已经在当前的 Java 平台中得到了复制。
java.base模块已更新以处理日志记录功能,不依赖java.util.loggingAPI。它有一个独立的外观和实现组件。这意味着,当使用第三方框架时,JDK 只需要提供实现组件并返回与请求日志框架一起工作的平台日志记录器。
如下图所示,我们使用java.util.ServiceLoaderAPI 加载LoggerFinder实现。如果在使用系统类加载器时找不到具体实现,JDK 将使用默认实现:

ServiceLoader API 的LoggerFinder实现
XML 目录
现代 Java 平台包括一个标准的 XMLCatalogAPI,以支持 OasisXMLCatalogs 标准 v1.1。新的 API 定义了目录和目录解析抽象,以便 JAXP 处理器可以使用它们。在本节中,我们将了解以下内容:
- OasisXML 目录标准
- JAXP 处理器
- 早期的 XML 目录
- 当前 XML 目录
OasisXML 目录标准
XML(可扩展标记语言)目录是由目录项组成的 XML 文档。每个条目将一个标识符与另一个位置配对。OASIS 是一个非盈利的财团,其使命是推进开放标准。他们在 2005 年发布了 XML 目录标准 1.1 版。本标准有两个基本用例:
- 将外部标识符映射到 URI 引用
- 将 URI 引用映射到另一个 URI 引用
下面是一个示例 XML 目录条目:
<public publicId="-//Packt Publishing Limited//Mastering Java9//EN" uri="https://www.packtpub.com/application-development/mastering-java-9"/>
JAXP 处理器
用于 XML 处理的 JavaAPI 称为 JAXP。顾名思义,这个 API 用于解析 XML 文档。有四个相关接口:
- DOM:文档对象模型解析
- SAX:用于 XML 解析的简单 API
- StAX:用于 XML 解析的流式 API
- XSLT:转换 XML 文档的接口
早期的 XML 目录
自从 JDK6 以来,Java 平台就有了一个内部目录解析器。由于没有公共 API,因此使用外部工具和库来访问其功能。进入现代 Java 平台,即版本 9、10 和 11,我们的目标是使内部目录解析器成为一个标准 API,以便通用和易于支持。
当前 XML 目录
Java9 提供的新的 XML 目录 API 遵循 OASISXML 目录标准 v1.1。以下是特性和功能亮点:
-
执行
EntityResolver。 -
执行
URIResolver。 -
可以通过
CatalogManager创建 XML 目录。 -
CatalogManager将用于创建CatalogResolvers。 -
将遵循 OASIS 打开目录文件语义:
-
将外部标识符映射到 URI 引用
-
将 URI 引用映射到另一个 URI 引用
-
CatalogResolvers将实现 JAXPEntityResolver接口。 -
CatalogResolvers将实现 JAXPURIResolver接口。 -
SAX
XMLFilter将由解析器支持。
因为新的 XML 目录 API 是公共的,所以 Java9 之前的内部目录解析器已经被删除,因为它不再是必需的。
集合
Java 编程语言不支持集合文本。将此功能添加到 Java 平台是在 2013 年提出的,并在 2016 年重新进行了讨论,但它只是作为一个研究建议而被公开,并不是为了将来的实现。
Oracle 对集合字面值的定义是一种语法表达式形式,其计算结果是聚合类型,例如数组、列表或映射。
当然,直到 Java9 发布。据报道,在 Java 编程语言中实现集合字面值具有以下好处:
- 性能改进
- 提高安全性
- 样板代码缩减
即使没有加入研究小组,我们对 Java 编程语言的了解也会给我们带来更多好处:
- 编写较短代码的能力
- 编写节省空间 d 的代码的能力
- 使集合字面值不可变的能力
让我们看两个案例,一个是使用现代 Java 平台之前的集合,另一个是使用新 Java 平台中对集合文本的新支持。
使用现代 Java 平台之前的集合
下面是一个示例,说明如何在现代 Java 平台之前创建自己的集合。第一个类定义了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);
}
}
以下是此应用的输出:

OldSchool类的输出
不幸的是,这段代码非常冗长。我们在静态初始化器块中填充集合,而不是使用字段初始化器。还有其他方法填充我们的列表,但它们都比应该的更冗长。这些其他方法还有其他问题,比如需要创建额外的类、使用晦涩的代码和隐藏的引用。
现在,让我们看看这个问题的解决方案,它是由现代 Java 平台提供的,我们将在下一节中介绍它的新特性。
使用新集合字面值
为了纠正创建集合时当前所需代码的冗长性,我们需要用于创建集合实例的库 API。请看上一节前面的代码片段,然后考虑以下可能的重构:
PlanetCollection<String> myPlanets = Set.of(
"Earth",
"Jupiter",
"Mars",
"Venus",
"Saturn",
"Mercury",
"Neptune",
"Uranus",
"Dagobah",
"Kobol");
这段代码是高度可读的,而不是冗长的。
新的实现将包括以下接口上的静态工厂方法:
ListMapSet
因此,我们现在可以创建不可修改的List集合、Map集合和Set集合实例。它们可以用以下语法实例化:
List.of(a, b, c, d, e);Set.of(a, b, c, d, e);Map.of();
Map集合将有一组固定参数。
特定于平台的桌面功能
现代 Java 平台包括一个公共 API,它使我们能够编写能够访问特定于平台的桌面功能的应用。这些功能包括与任务栏/工作台交互以及监听应用和系统事件。
MacOSXcom.apple.eawt包是一个内部 API,从 Java9 开始,就不能再访问了。为了支持 Java 的嵌入式平台特定的桌面特性,apple.applescript类被删除而不进行替换。它们在 Java9、10 或 11 中不可用。
新 API 已添加到java.awt.Desktop类中,并提供以下内容:
-
它创建了一个公共 API 来替换
com.apple.{east,eio}中的功能。 -
它确保了 OSX 开发人员不会丢失功能。为此,当前的 Java 平台替换了以下包:
-
com.apple.eawt -
com.apple.eio -
除了 OS X 之外,它还为开发人员提供了一套近乎通用的平台(即 Windows 和 Linux)功能。这些通用功能包括:
-
带有事件监听器的登录/注销处理器
-
带有事件监听器的屏幕锁处理器
-
任务栏/停靠操作包括以下内容:
-
请求用户注意
-
指示任务进度
-
动作快捷方式
增强的方法句柄
现代 Java 平台包括增强的方法句柄,作为改进以下列出的类的一种方法,以便通过改进的优化简化常见用法:
MethodHandle类MethodHandles类MethodHandles.Lookup类
前面的类都是java.lang.invoke包的一部分,该包已针对现代 Java 平台进行了更新。这些改进是通过使用MethodHandle组合、for循环和try...finally块的查找细化实现的。
在本节中,我们将了解以下内容:
- 增强的原因
- 查找函数
- 参数处理
- 额外组合
增强的原因
这种增强源于开发人员的反馈,以及使MethodHandle、MethodHandles和MethodHandles.Lookup类更易于使用的愿望,还有添加额外用例的呼吁。
这些变化带来了以下好处:
- 在使用
MethodHandleAPI 时启用的精度 - 实例化缩减
- 增加的 JVM 编译器优化
查找函数
有关查找函数的更改包括:
MethodHandles现在可以绑定到接口中的非抽象方法- LookupAPI 允许从不同的上下文进行类查找
MethodHandles.Lookup.findSpecial(Class<?> refs, String name, MethodType type, Class<?> specialCaller)类已被修改,以允许在接口上定位超级可调用方法。
另外,在MethodHandles.Lookup类中增加了以下方法:
Class<?> findClass(String targetName)Class<?> accessClass(Class<?> targetClass)
参数处理
最近进行了三次更新以改进MethodHandle参数处理。这些变化如下:
-
使用
foldArguments(MethodHandle target, MethodHandle combinator)的参数折叠以前没有位置参数:- 使用
MethodHandle.asCollector(Class<?> arrayType, int arrayLength)方法的参数集合以前不支持将参数集合到数组中,但尾部元素除外。这一点已经改变,现在有一个额外的asCollector方法来支持该功能。
- 使用
-
在参数集合的反向方法中,使用
MethodHandle.asSpreader(Class<?> arrayType, int arrayLength)方法的参数扩展将尾部数组的内容扩展到多个参数。已修改参数扩展,以支持在方法签名的任何位置扩展数组。
下一节将提供更新的asCollector和asSpreader方法的新方法定义。
额外组合
添加了以下额外组合以支持java.lang.invoke包的MethodHandle、MethodHandles和MethodHandles.Lookup类的易用性和优化:
-
通用循环抽象:
-
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)
废弃的改进
有两种表达反对意见的工具:
@Deprecated注解@deprecatedJavadoc 标签
这些工具分别在 JavaSE5 和 JDK1.1 中引入。@Deprecated注解的目的是注解那些不应该使用的程序组件,因为它们被认为是危险的和/或有更好的选择。这就是预期用途,实际用途各不相同,而且由于警告只在编译时提供,因此几乎没有理由忽略带注解的代码。
增强的弃用工作是为了向开发人员提供关于规范文档中 API 的预期配置的更清晰的信息。这方面的工作还产生了一个分析程序使用不推荐的 API 的工具。
为了支持信息的保真度,以下组件被添加到java.lang.Deprecated注解类型中:
-
forRemoval(): -
返回布尔值
true,如果 API 元素已被安排在将来删除 -
如果 API 元素未被指定为将来删除,但已弃用,则返回布尔值
false -
默认为
false -
since(): -
返回包含版本号或版本号的字符串,此时指定的 API 被标记为已弃用
@Deprecated注解的真正含义
当一个 API 或 API 中的方法已标记有@Deprecated注解时,通常存在以下一个或多个条件:
- API 中存在错误,没有计划修复这些错误
- 使用 API 可能会导致错误
- API 已被另一个 API 替换
- API 是实验性的
本机头生成工具(javah)
Java 头工具(javah是用 Java8 引入 Java 平台的。它为开发人员提供了编写本机头的能力。从 Java10 开始,javah工具被 Java 编译器(javac中包含的功能所取代
开发人员不使用javah,而只使用javac -h。
总结
在本章中,我们介绍了有关现代平台的几个升级。这些更新涵盖了广泛的工具和 API 更新,使使用 Java 进行开发变得更容易,并为我们生成的程序提供了更大的优化可能性。我们回顾了新的 HTTP 客户端、对 Javadoc 和 Doclet API 的更改、新的 JavaScript 解析器、JAR 和 JRE 更改、新的 Java 级 JVM 编译器接口、对 TIFF 图像的新支持、平台日志记录、XML 目录支持、集合以及新的平台特定桌面功能。我们还研究了方法处理和弃用注解的增强功能。
在下一章中,我们将讨论并发增强,我们的主要关注点是对Flow类 API 提供的反应式编程的支持。我们还将探讨额外的并发增强。
问题
- 升级 HTTP 客户端的主要原因是什么?
- 列出新的 HTTP 客户端 API 的限制。
- 使用
javadoc必须传递哪三个组件? - 命名一个或多个属于 Doclet 类的方法(构造器除外)。
- 编译器树 API 中的枚举是什么?
- Javadoc 工具的默认输出是什么?
- Nashorn 是什么?
- 什么是 ECMAScript?
- 说出两个主要的 JAR 文件增强。
JavaBean的三个约定是什么?
进一步阅读
以下是您可以参考的信息列表:
十二、并发性增强
在上一章中,我们讨论了现代 Java 平台的几个增强。这些增强代表了一系列工具和 API 的更新,以使使用 Java 开发更容易,并为我们的 Java 应用提供了更大的优化可能性。我们研究了新的 HTTP 客户端、对 Javadoc 和 Doclet API 的更改、新的 JavaScript 解析器、JAR 和 JRE 更改、新的 Java 级 JVM 编译器接口、对 TIFF 图像的新支持、平台日志记录、XML 目录支持、集合以及新的特定于平台的桌面功能。我们还研究了对方法处理和弃用注解的增强。
在本章中,我们将介绍 Java 平台的并发增强。我们主要关注的是对反应式编程的支持,这是一种由Flow 类 API 提供的并发增强。反应式编程最初是在 Java9 中发布的,它仍然是 Java10 和 Java11 的一个重要特性。我们还将探讨额外的并发增强。
更具体地说,我们将讨论以下主题:
- 反应式程序设计
FlowAPI- 其他并发更新
- 旋转等待提示
技术要求
本章以及随后的几章介绍 Java11。Java 平台的 SE 可从 Oracle 官方网站下载。
集成开发环境(IDE)包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
反应式程序设计
反应式编程是指应用在异步数据流发生时对其作出反应。下图说明了此流程:

反应式编程流程
反应式编程并不是一个仅由学术界使用的奇特的软件工程术语。事实上,它是一种编程模型,可以提高效率,而不是让应用在内存中的数据上迭代的更常见方法。
有更多的反应式编程。首先,让我们考虑一下数据流是由发布者以异步方式提供给订阅服务器的。
数据流是字符串和原始数据类型的二进制输入/输出,DataInput接口用于输入流,DataOutput 接口用于输出流。
处理器或处理器链可用于转换数据流,而无需发布者或订阅者参与。在下面的例子中,处理器在没有发布者或订户参与,甚至没有意识到的情况下处理数据流:

处理器-订户关系
除了更高的效率之外,反应式编程还带来了一些额外的好处,这些好处强调如下:
-
代码库可以不那么冗长,因此:
-
更容易编码
-
易于维护
-
更容易阅读
-
流处理可提高内存效率
-
这是一个针对各种编程应用的解决方案
-
需要编写的样板代码更少,因此开发时间可以集中在编程核心功能上
-
以下类型的编程需要较少的时间和代码:
-
并发
-
低级线程
-
同步
反应式程序设计标准化
软件开发的许多方面都有标准,而反应式编程也没有逃脱这一点。有一个反应流计划来标准化异步流处理。在 Java 上下文中,具体的焦点是 JVM 和 JavaScript。
Reactive 流计划旨在解决控制线程之间如何交换数据流的问题。正如您在上一节中所记得的,处理器的概念是基于对发布者或接收器没有影响。本无影响授权书规定,不需要以下内容:
- 数据缓冲
- 数据平移
- 转换
该标准的基本语义定义了数据流元素传输的规则。这个标准是专门为 Java9 平台而建立的。Reactive 流包含一个库,可以帮助开发人员在org.reactivestreams和java.util.concurrent.Flow名称空间之间进行转换。
成功使用反应式编程和反应式流标准化的关键是理解相关术语:
| 术语 | 说明 |
|---|---|
| 需求 | 需求是指订阅者对更多元素的请求,以及发布者尚未满足的元素请求总数。 |
| 需求 | 需求还指发布者尚未满足的请求元素总数。 |
| 外部同步 | 线程安全的外部访问协调。 |
| 无阻碍 | 如果方法在不需要大量计算的情况下快速执行,则称其为无障碍方法。非阻塞方法不会延迟订阅服务器的线程执行。 |
| 没有 | NOP 执行是可以重复调用而不影响调用线程的执行。 |
| 响应度 | 这个术语是指组件的响应能力。 |
| 正常返回 | 正常返回是指当没有错误时的正常状态。onError方法是标准允许的唯一通知用户故障的方法。 |
| 信号 | 以下方法之一: |
cancel() |
|
onComplete() |
|
onError() |
|
onSubscribe() |
|
request() |
在下一节中,我们将研究 Java 平台中的Flow API,因为它们对应于反应流规范。
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 subscribe(Flow.Subscriber subscriber)。
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.Publisher和Flow.Subscriber实例之间消息的链接。它有以下方法:
void cancel()void request(long n)
Flow.Processor接口
Flow.Processor接口可以同时作为Subscriber和Publisher。此处提供了实现:
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
}
@Override
public void onSubscribe(Subscription theSubscription) {
this.theSubscription = theSubscription;
theSubscription.request(19);
}
}
如您所见,实现Subscriber并不困难。繁重的工作由位于Subscriber和Publisher之间的处理器完成。让我们看一个示例实现,Publisher向订阅者发布数据流:
import java.util.concurrent.SubsmissionPublisher;
. . .
// First, let's create a Publisher instance
SubmissionPublisher<String> packtPublisher =
newSubmissionPublisher<>();
// 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 平台最近得到了增强,以改进并发性的使用。在本节中,我们将简要探讨 Java 并发的概念,并查看 Java 平台的相关增强功能,包括:
- Java 并发
- 反应流的支持
CompletableFutureAPI 增强
Java 并发
在本节中,我们将从并发的简要说明开始,然后看系统配置,介绍 Java 线程,最后看并发的改进。
并发性解释
并行处理从 20 世纪 60 年代就开始了,在那些形成的年代,我们已经有了允许多个进程共享一个处理器的系统。这些系统被更清楚地定义为伪并行系统,因为它看起来只是多个进程同时被执行。时至今日,我们的计算机仍以这种方式运行。20 世纪 60 年代和现在的区别在于,我们的计算机可以有多个 CPU,每个 CPU 都有多个内核,这更好地支持并发。
并发性和并行性经常被用作可互换的术语。并发是指当多个进程重叠时,尽管开始和停止时间可能不同。并行性发生在任务同时启动、运行和停止时。
系统配置
需要考虑几种不同的处理器配置。本节提供两种常见配置。第一种配置是共享内存的配置,如下所示:

共享内存配置
如您所见,共享内存系统配置有多个处理器,它们共享一个公共系统内存。第二个特色系统配置是分布式内存系统:

分布式存储系统
在分布式存储系统中,每个处理器都有自己的内存,每个单独的处理器都与其他处理器完全链接,形成了一个完全链接的分布式系统。
Java 线程
Java 中的线程是一个程序执行,内置在 JVM 中。Thread类是java.lang包(java.lang.Thread的一部分。线程具有控制 JVM 执行它们的顺序的优先级。虽然概念很简单,但实现却不简单。让我们先来仔细看看Thread类。
Thread类有一个嵌套类:
public static enum Thread.State
与Thread类相关的还有以下接口:
public static interface Thread.UncaughtExceptionHandler
有三个类变量用于管理线程优先级:
public static final int MAX_PRIORITYpublic static final int MIN_PRIORITYpublic static final int NORM_PRIORITY
Thread类有八个构造器,它们都分配一个新的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)
Thread类还有 43 个方法,其中 6 个已经被弃用。剩下的方法在这里列出,除了分别列出的访问器和变异器。有关每种方法的详细信息,请参阅文档:
-
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.UncaughtException Handler 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 并发功能的核心。我们可以通过定义一个run方法并实例化一个Thread对象,在 Java 中创建一个线程。完成这组任务有两种方法。我们的第一个选择是扩展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 增强
CompletableFuture<T>类是java.util.concurrent包的一部分。该类扩展了Object类,实现了Future<T>和CompletionStage<T>接口。此类用于标注可以完成的线程。我们可以使用CompletableFuture类来表示未来的结果。当使用complete方法时,可以完成将来的结果。
重要的是要认识到,如果多个线程试图同时完成(完成或取消),除一个线程外,其他所有线程都将失败。让我们看看这个类,然后看看增强功能。
类详情
CompletableFuture<T>类有一个嵌套类,用于标记异步任务:
public static interface CompletableFuture.AsynchronousCompletionTask
CompletableFuture<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> CompletedStage<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)
这些方法返回CompletableFuture:
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)
这些方法返回boolean:
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, TimeoutExceptionpublic T get() throws InterruptedException, ExecutionExceptionpublic T getNow(T valueIfAbsent)public int getNumberOfDependents()public T join()public String toString()
增强
作为当前 Java 平台的一部分,CompletableFuture<T>类收到了以下增强:
-
添加的基于时间的增强功能:
-
这样可以根据经过的时间来完成
-
现在也支持延迟执行
-
子类显著增强:
- 扩展
CompletableFuture更容易 - 子类支持可选的默认执行器
- 扩展
具体来说,Java9 中添加了以下方法:
newIncompleteFuture()defaultExecutor()copy()minimalCompletionStage()completeAsync()orTimeout()completeOnTimeout()delayedExecutor()completedStage()failedFuture()failedStage()
旋转等待提示
对于并发,我们需要确保等待执行的线程实际得到执行。自旋等待的概念是一个不断检查真实情况的过程。Java 平台有一个 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 平台的并发增强。我们将并发作为一个核心 Java 概念进行了深入的研究,并着眼于 Java 提供了什么。我们还研究了支持反应式编程的Flow类 API。此外,我们还探讨了并发增强和新的旋转等待提示。
在下一章中,我们将重点介绍 Java 平台的安全增强功能,以及实际示例。
问题
- 什么是反应式编程?
- 什么是数据流?
- 使用反应式编程的主要好处是什么?
- 反应式编程的无影响授权有哪些规定?
Flow类是什么包?- 列出
Flow类的四个接口。 - 什么是并发性?
- 并发和并行的区别是什么?
- 解释共享内存系统配置。
- 解释分布式内存系统配置。
进一步阅读
以下是您可以参考的信息列表:
十三、安全增强功能
在最后一章中,我们讨论了现代 Java 平台的并发增强。我们深入研究了并发性,它既是一个核心概念,也是 Java 的一系列增强。我们还研究了支持反应式编程的Flow类 API。此外,我们还探讨了 Java 的并发增强和旋转等待提示。
在本章中,我们将介绍最近对 JDK 所做的几个涉及安全性的更改。这些变化的大小并不反映其重要性。现代 Java 平台的安全增强为开发人员提供了编写和维护比以前更安全的应用的能力。
更具体地说,我们将在本章中回顾以下主题:
- 数据报传输层安全
- 创建 PKCS12 密钥库
- 提高安全应用性能
- TLS 应用层协议协商扩展
- 利用 GHASH 和 RSA 的 CPU 指令
- 用于 TLS 的 OCSP 装订
- 基于 DRBG 的
SecureRandom实现
技术要求
本章和随后的几章主要介绍 Java11。Java 平台的标准版(SE)可从 Oracle 官网下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
数据报传输层安全
数据报传输层安全(DTLS)是一种通信协议。该协议为基于数据报的应用提供了一个安全层。DTLS 允许安全通信,基于传输层安全(TLS)协议。嵌入式安全性有助于确保消息不被伪造、篡改或窃听。
让我们回顾一下相关术语:
- 通信协议:一组控制信息传输方式的规则。
- 数据报:结构化传输单元。
- 窃听:监听在途数据包时未被发现。
- 伪造:用伪造的发送者传送数据包。
- 网络包:一种格式化的数据传输单元。
- 篡改:在发送方发送数据包之后,在预定接收方接收数据包之前,对数据包的篡改。
- TLS 协议:最常用的网络安全协议。例如,它使用 IMPA 和 POP 发送电子邮件。
最近的 DTLS Java 增强旨在为 DTLS 的 1.0 和 1.2 版本创建 API。
在接下来的部分中,我们将查看每个 DTLS 版本 1.0 和 1.2,然后回顾对 Java 平台所做的更改。
DTLS 协议版本 1.0
DTLS 协议 1.0 版于 2006 年建立,为数据报协议提供通信安全。其基本特征如下:
-
允许客户端/服务器应用通信,而不允许:
-
窃听
-
篡改
-
信息伪造
-
基于 TLS 协议
-
提供安全保障
-
保留了 DLS 协议的数据报语义
下图说明了传输层在 SSL/TLS 协议层的总体架构中的位置以及每层的协议:

SSL/TLS 协议层
DTLS 协议版本 1.0 提供了主要覆盖区域的详细规范,如下所示:
-
密码:
-
防重放分组密码
-
新密码套件
-
标准(或空)流密码
-
拒绝服务对策
-
握手:
-
消息格式
-
协议
-
可靠性
-
信息:
-
分裂与重组
-
对丢失不敏感的消息
-
大小
-
超时和重传
-
数据包丢失
-
路径最大转换单元(PMTU)发现
-
记录层
-
记录有效负载保护
-
重新排序
-
重放检测
-
传输层映射
DTLS 协议版本 1.2
DTLS 协议 1.2 版于 2012 年 1 月发布,版权归互联网工程任务组(IETF)所有。本节共享说明在版本 1.2 中所做更改的代码示例。
下面的代码演示了 TLS1.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 有关文件的法律规定重新发布。
记录层包含我们打算发送到记录中的信息。信息开始于DTLSPlaintext结构中,然后在握手发生之后,记录被加密,并且可以通过通信流发送。记录层格式遵循 1.2 版中的新字段,并在代码注释中用// New field注解,如下所示:
// 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^8-1>; // New field
CipherSuite cipher_suites<2..2^16-1>;
CompressionMethod compression_methods<1..2^8-1>; } ClientHello;
struct {
ProtocolVersion server_version;
opaque cookie<0..2^8-1>; } HelloVerifyRequest;
Java 中的 DTLS 支持
DTLS API 的 Java 实现是独立于传输的,而且是轻量级的。API 的设计考虑如下:
- 将不管理读取超时
- 实现将为每个包装/展开操作使用一个 TLS 记录
- 应用(而不是 API)需要:
- 确定超时值
- 组装无序的应用数据
DTLS 是一种协议,用于在将数据传递到传输层协议之前保护来自应用层的数据。DTLS 是加密和传输实时数据的一个很好的解决方案。应谨慎行事,以免在应用实现中引入漏洞。以下是在 Java 应用中实现 DTL 的一些安全注意事项:
- 实现 DTLS 1.2,因为它是 Java 支持的最新版本。
- 避免 Rivest Shamir Adleman(RSA)加密。如果必须使用 RSA,请为私钥添加额外的安全性,因为这是 RSA 的一个弱点。
- 当使用椭圆曲线 Diffie-Hellman(ECDH)匿名密钥协商协议时,使用 192 位或更多。192 位的值基于美国国家标准与技术研究所(NIST)的建议。
- 强烈建议使用带有相关数据的认证加密(AEAD),这是一种加密形式。AEAD 为加密和解密的数据提供真实性、机密性和完整性保证。
- 在实现握手重新协商时,始终实现
renegotiation_info扩展。 - 在使用通信协议的所有 Java 应用中建立前向保密(FS)功能。实现 FS 可以确保过去的会话加密密钥不会在长期加密密钥受损时受损。理想情况下,完美前向保密(PFS),其中每个密钥仅对单个会话有效,将用于要求传输数据最大安全性的 Java 应用中。
创建 PKCS12 密钥库
Java 平台为密钥库提供了更高的安全性。在默认情况下创建 PKCS12 密钥库之前,我们将首先回顾密钥库的概念,查看KeyStore类,然后查看 Java 平台的最新更新。
密钥库入门
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。 |
这个类从 1.2 版开始就是 Java 平台的一部分。它有一个构造器、三个接口、六个子类和几个方法。构造器定义如下:
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类还包含六个嵌套类,每个嵌套类都将在后面的部分中进行研究。
KeyStoreSpi类定义密钥存储的服务提供者接口(SPI)。
了解密钥库生成器
KeyStore.Builder类用于延迟KeyStore的实例化:
public abstract static class KeyStore.Builder extends Object
这个类为实例化一个KeyStore对象提供了必要的信息。该类具有以下方法:
public abstract KeyStore getKeyStore() throws KeyStoreException。public abstractKeyStore.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链返回结束实体Certificatepublic Certificate[] getCertificateChain():返回Certificate链作为Certificates的数组public PrivateKey getPrivateKey():返回当前分录的PrivateKeypublic String toString():返回PrivateKeyEntry为String
SecretKeyEntry类
KeyStore.SecretKeyEntry类定义如下:
public static final class KeyStore.SecretKeyEntry extends Object implements KeyStore.Entry
这个类持有SecretKey,有以下方法:
public SecretKey getSecretKey():返回分录的SecretKeypublic String toString():返回SecretKeyEntry为String
TrustedCertificateEntry类
KeyStore.TrustedCertificateEntry类定义如下:
public static final class KeyStore.TrustedCertificateEntry extends Object implements KeyStore.Entry
此类持有一个可信的Certificate,并具有以下方法:
public Certificate getTrustedCertificate():返回条目的可信Certificatepublic 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 "packt.pub" alias
myKS.setCertificateEntry("packt.pub",
myCertEntry.getTrustedCertificate());
return myKS;
}
catch (Exception e) {
throw new AssertionError(e);
}
}
. . .
Java9、10 和 11 中的 PKCS12 默认值
在 Java9 之前,默认的KeyStore类型是 Java 密钥库(JKS)。当前的 Java 平台使用 PKCS 作为默认的KeyStore类型,更确切地说,PKCS12。
PKCS 是公钥密码标准的首字母缩写。
与 JKS 相比,PKCS 的这种变化提供了更强的加密算法。正如您所料,JDK9、10 和 11 仍然与 JKS 兼容,以支持以前开发的系统。
提高安全应用性能
当运行安装了安全管理器的应用时,现代 Java 平台包括性能改进。安全管理器可能导致处理开销和不理想的应用性能。
这是一项令人印象深刻的任务,因为当前运行安全管理器时的 CPU 开销估计会导致 10-15% 的性能下降。完全消除 CPU 开销是不可行的,因为运行安全管理器需要一些 CPU 处理。也就是说,目标是尽可能降低间接费用的百分比。
这项工作导致了以下优化,每个优化将在后面的部分中详细介绍:
- 安全策略实现
- 权限评估
- 哈希码
- 包检查算法
安全策略实现
JDK 使用ConcurrentHashMap将ProtectionDomain映射到PermissionCollection。ConcurrentHashMap通常用于应用中的高并发性。它具有以下特点:
- 线程安全
- 进入映射不需要同步
- 快速读取
- 使用锁的写入
- 无对象级锁定
- 非常精细的级别上的锁定
ConcurrentHashMap类定义如下:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable
在前面的类定义中,K表示哈希映射维护的键的类型,V表示映射值的类型。有一个KeySetView子类和几个方法。
与强制执行安全策略相关的附加类有三个-ProtectionDomain、PermissionCollection和SecureClassLoader:
ProtectionDomain类用于封装一组类,以便向域授予权限。PermissionCollection类表示权限对象的集合。SecureClassLoader类扩展了ClassLoader类,它提供了额外的功能,用于定义具有系统策略检索权限的类。在 Java 中,这个类使用ConcurrentHashMap来提高安全性。
权限评估
在权限评估类别下,进行了三项优化:
identifyPolicyEntries列表以前有用于同步的策略供应器代码。此代码已被删除,在 Java9、10 或 11 中不可用。PermissionCollection条目现在存储在ConcurrentHashMap中。它们以前被存储为Permission类中的HashMap。- 权限现在存储在
PermissionCollection的子类中的并发集合中。
java.security.CodeSource包
哈希码是一个对象生成的数字,存储在哈希表中,用于快速存储和检索。Java 中的每个对象都有一个哈希码。以下是哈希码的一些特征和规则:
- 哈希码对于正在运行的进程中的相等对象是相同的
- 哈希码可以在执行周期之间更改
- 哈希码不应用作密钥
Java 平台包括一个改进的hashCode方法java.security.CodeSource来优化 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的信息,包括位置和证书。
包检查算法
当运行安装了安全管理器的应用时,Java 最近的性能改进是以java.lang.SecurityManager包增强的形式出现的。更具体地说,checkPackageAccess方法的包检查算法被修改。
java.lang.SecurityManager类允许应用在特定操作上实现安全策略。此类的public void checkPackageAccess(String pkg)方法从getProperty()方法接收逗号分隔的受限包列表。如这里所示,根据评估,checkPackageAccess方法可以抛出两个异常中的一个:

checkPackageAccess方法的异常
TLS 应用层协议协商扩展
javax.net.ssl包最近进行了增强,支持传输层安全扩展(TLS ALPN)(简称应用层协议协商)。此扩展允许 TLS 连接的应用协议协商。
TLS ALPN 扩展
ALPN 是 TLS 扩展,可用于协商在使用安全连接时要实现的协议。ALPN 是协商协议的有效手段。如下图所示,TLS 握手有五个基本步骤:

TLS 握手的五个步骤
java.net.ssl包
java.net.ssl包包含与安全套接字包相关的类。这允许我们以 SSL 为例,可靠地检测引入网络字节流的错误。它还提供了加密数据以及提供客户端和服务器认证的能力。
此包包括以下接口:
public interface HandshakeCompletedListener extends EventListenerpublic interface HostnameVerifierpublic interface KeyManagerpublic interface ManagerFactoryParameterspublic interface SSLSessionpublic interface SSLSessionBindingListener extends EventListenerpublic interface SSLSessionContextpublic interace TrustManagerpublic interface X509KeyManager extends KeyManagerpublic interface X509TrustManager extends TrustManager
java.net.ssl包还有以下子类:
public class CertPathTrustManagerParameters extends Object implements ManagerFactoryParameterspublic abstract class ExtendedSSLSession extends Object implements SSLSessionpublic class HandshakeCompleteEvent extends EventObjectpublic abstract class HttpsURLConnection extends HttpURLConnectionpublic class KeyManagerFactory extends Objectpublic abstract class KeyManagerFactorySpipublic class KeyStoreBuilderParameters extends Object implements ManagerFactoryParameterspublic class SSLContext extends Objectpublic abstract class SSLContextSpi extends Objectpublic abstract class SSLEngine extends Objectpublic class SSLEngineResult extends Objectpublic class SSLParameters extends Objectpublic final class SSLPermission extends BasicPermissionpublic abstract class SSLServerSocket extends ServerSocketpublic abstract class SSLServerSocketFactory extends ServerSocketFactorypublic class SSLSessionBindingEvent extends EventObjectpublic abstract class SSLSocket extends Socketpublic abstract class SSLSocketFactory extends SocketFactorypublic class TrustManagerFactory extends Objectpublic abstract class TrustManagerFactorySpi extends Objectpublic abstract class X509ExtendedKeyManager extends Object implements X509KeyManagerpublic abstract class X509ExtendedTrustManager extends Object implements x509TrustManager
java.net.ssl包扩展
Java 平台中对java.net.ssl包的这个更改使得它现在支持 TLS-ALPN 扩展。这一变化的主要好处如下:
- TLS 客户端和服务器现在可以使用多个应用层协议,这些协议可以使用也可以不使用同一传输层端口
- ALPN 扩展允许客户端对其支持的应用层协议进行优先级排序
- 服务器可以为 TLS 连接选择客户端协议
- 支持 HTTP/2
下面的说明是 TLS 握手的五个基本步骤。针对 Java9 进行了更新并在此处显示,下图显示了在客户端和服务器之间共享协议名称的位置:

TLS 握手:共享协议名称
一旦接收到客户端的应用层协议列表,服务器就可以选择服务器的首选交集值,并从外部扫描初始明文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^16-1>;
CompressionMethod compression_methods<1..2^8-1>;
Extension extensions<0..2^16-1>;
} ClientHello;
利用 GHASH 和 RSA 的 CPU 指令
现代 Java 平台包括一个改进的加密操作性能,特别是 GHASH 和 RSA。通过利用最新的 SPARC 和 IntelX64CPU 指令,Java 实现了这种性能改进。
此增强不需要新的或修改的 API 作为 Java 平台的一部分。
散列
Galois HASH(GHASH)和 RSA 是密码系统哈希算法。哈希是由文本字符串生成的固定长度的字符串或数字。算法,更具体地说是散列算法,被设计成这样的结果散列不能被反向工程。我们使用散列存储用盐生成的密码。
在密码学中,盐是一种随机数据,用作哈希函数生成密码的输入。盐有助于防止彩虹表攻击和字典攻击。
下图说明了哈希的基本工作原理:

哈希概述
如您所见,散列算法被输入明文和盐,从而产生一个新的散列密码并存储盐。以下是带有示例输入/输出的相同图形,以演示功能:

哈希和盐功能
如下图所示,验证过程从用户输入纯文本密码开始。散列算法接受纯文本并用存储的盐重新散列它。然后,将得到的哈希密码与存储的密码进行比较:

哈希匹配验证
用于 TLS 的 OCSP 装订
在线证书状态协议(OCSP)装订是检查数字证书撤销状态的方法。确定 SSL 证书有效性的 OCSP 装订方法被评估为既安全又快速。通过允许 Web 服务器提供其组织证书的有效性信息,而不是从证书的颁发供应商处请求验证信息的较长过程,可以实现确定速度。
OCSP 装订以前被称为 TLS 证书状态请求扩展。
OCSP 装订入门
OCSP 装订过程涉及多个组件和有效性检查。下图说明了 OCSP 装订过程:

哈希匹配验证
如您所见,当用户试图通过浏览器打开 SSL 加密的网站时,该过程就开始了。浏览器查询 Web 服务器以确保 SSL 加密的网站具有有效的证书。Web 服务器查询证书的供应商,并提供证书状态和数字签名的时间戳。Web 服务器获取这两个组件,将它们装订在一起,并将装订好的集合返回到请求的浏览器。然后,浏览器可以检查时间戳的有效性,并决定是显示 SSL 加密的网站还是显示错误。
Java 平台的最新变化
TLS 的 OCSP 装订通过 TLS 证书状态请求扩展实现 OCSP 装订。OSCP 装订检查 X.509 证书的有效性。
X.509 证书是使用 X509 公钥基础设施(PKI)的数字证书。
在 Java9 之前,可以在客户端启用证书有效性检查(实际上,检查证书是否已被吊销),但效率低下:
- OCSP 响应程序的性能瓶颈
- 基于多通道的性能下降
- 如果在客户端执行 OCSP 检查,则性能会进一步降低
- 在浏览器未连接到 OCSP 响应程序时失败
- OCSP 响应程序易受拒绝服务攻击
用于 TLS 的新 OCSP 装订包括 Java9、10 和 11 的以下系统属性更改:
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:- 默认设置:无
- 可以为没有权限信息访问(AIA)扩展的证书设置默认 URI
- 除非设置了
jdk.tls.stapling.Override属性,否则不覆盖 AIA 扩展
jdk.tls.stapling.respoderOverride:- 默认设置:
false - 允许
jdk.tls.stapling.responderURI提供的属性覆盖 AIA 扩展值
- 默认设置:
jdk.tls.stapling.ignoreExtensions:- 默认设置:
false - 禁用 OCSP 扩展转发,如
status_request或status_request_v2TLS 扩展中所述
- 默认设置:
status_request和status_request_v2TLS Hello 扩展现在都受客户端和服务器端 Java 实现的支持。
基于 DRBG 的SecureRandom实现
在 Java 的早期版本中,即版本 8 和更早版本中,JDK 有两种生成安全随机数的方法。有一种方法是用 Java 编写的,使用基于 SHA1 的随机数生成,而且不是很强。另一种方法依赖于平台,使用预配置的库。
确定性随机位生成器(DRBG)是一种产生随机数的方法。它已经被美国商务部的分支机构 NIST 批准。DRBG 方法包括生成安全随机数的现代和更强的算法。
最近,实现了三种特定的 DRBG 机制。这些机制如下:
Hash_DRBGHMAC_DRBGCTR_DRBG
您可以在这个页面了解每个 DRBG 机制的细节。
以下是三个新的 API:
SecureRandom:新方法,允许配置具有以下可配置属性的SecureRandom对象:- 播种
- 重新播种
- 随机位生成
SecureRandomSpi:实现SecureRandom方法的新方法。SecureRandomParameter:新的接口,以便输入可以传递给新的SecureRandom方法。
总结
在本章中,我们研究了 JDK 中涉及安全性的几个小而重要的更改。特色的安全增强功能为开发人员提供了编写和维护实现安全性的应用的独特能力。更具体地说,我们讨论了 DTL、密钥库、提高安全应用性能、TLS ALPN、利用 GHASH 和 RSA 的 CPU 指令、TLS 的 OCSP 装订以及基于 DRBG 的SecureRandom实现。
在下一章中,我们将探讨 Java 中使用的新命令行标志以及对各种命令行工具的更改。我们的内容将包括使用新的命令行选项和标志管理 Java 的 JVM 运行时和编译器。
问题
- 什么是 DTLS?
- 什么是 TLS?
- 握手重新协商的安全考虑是什么?
- 为什么要在 Java 应用中建立 FS 功能?
- 什么是
KeyStore? KeyStore存放在哪里?Builder类的目的是什么?ConcurrentHashMap有什么特点?- 什么是哈希码?
- 什么是 GHASH?
进一步阅读
以下是您可以参考的信息列表:
- 《即时 Java 密码和认证安全》,在这个页面提供。
十四、命令行标志
在上一章中,我们研究了 JDK 的几个安全性更改。Java 的安全增强为开发人员提供了编写和维护实现安全性的应用的能力。更具体地说,我们讨论了数据报传输层安全性、密钥库、提高安全应用性能、TLS ALPN、利用 GHASH 和 RSA 的 CPU 指令、TLS 的 OCSP 装订以及基于 DRBG 的SecureRandom实现。
在本章中,我们将探讨现代 Java 平台的几个变化,这些变化的共同主题是命令行标志。更具体地说,我们将介绍以下概念:
- 统一 JVM 日志记录
- 编译器控件
- 诊断命令
- 堆分析代理
- 移除 JHAT
- 命令行标志参数验证
- 为旧平台版本编译
- 基于 Java 的 JIT 编译器实验
技术要求
本章及后续章节主要介绍 Java11,Java 平台的标准版(SE)可从 Oracle 官方网站下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
统一 JVM 日志记录
Java9 中引入了为 JVM 创建统一的日志模式。以下是这项工作的目标的综合清单:
-
为所有日志操作创建一组 JVM 范围的命令行选项
-
使用分类标签进行日志记录
-
允许消息具有多个标签,也称为标签集
-
要提供六个级别的日志记录:
-
错误
-
警告
-
信息
-
调试
-
跟踪
-
开发
-
根据级别选择要记录的消息
-
要选择性地将日志记录定向到控制台或文件,请执行以下操作:
-
一次打印一行,不支持在同一行内交错
-
允许输出多行日志(非交错)
-
设置所有日志消息的格式,使其易于人阅读
-
添加装饰,如正常运行时间、级别和标记
-
与级别类似,用于选择基于装饰记录哪些消息
-
将 Java9 之前的
tty>print日志转换为使用统一日志作为输出 -
允许使用
jcmd和MBeans进行动态消息配置 -
允许启用和禁用单个日志消息
-
添加确定装饰打印的顺序的功能
对 JVM 的统一日志记录更改可以分为以下五类:
- 命令行选项
- 装饰
- 水平
- 输出
- 标签
让我们简单地看一下这些类别中的每一个。
命令行选项
新的命令行选项-Xlog是 Java 日志框架的关键组件。这个命令行选项有大量的参数和可能性。基本语法是-Xlog,后跟一个选项。
以下是正式的基本语法:
-Xlog[:option]
下面是一个带有all选项的基本示例:
-Xlog:all
以下是用于配置新的统一日志记录的广泛命令行语法:
-Xlog[:option]
option := [<what>][:[<output>][:[<decorators>][:<outputoptions>]]]
'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 执行以下操作:
- 记录至少带有
gc和rt标记的所有消息 - 使用
debug水平 - 向
stdout提供输出
以下示例将输出推送到外部文件:
-Xlog:disable - Xlog:rt=debug:rtdebug.txt
在前面的示例中,我们告诉 JVM 执行以下操作:
- 禁用除标记有
rt标记的消息以外的所有消息 - 使用
debug水平 - 向名为
rtdebug.txt的文件提供输出
装饰
在 Java 日志框架的上下文中,装饰是关于日志消息的元数据。以下是按字母顺序排列的可用装饰品列表:
level:与记录的消息相关联的级别pid:进程标识符tags:与记录的消息相关联的标签集tid:线程标识符time:指当前日期和时间,采用 ISO-8601 格式timemillis:当前时间(毫秒)timenanos:当前时间(纳秒)uptime:JVM 启动后的时间,以秒和毫秒为单位uptimemillis:JVM 启动后的时间,以毫秒为单位uptimenanos:JVM 启动后的时间,以纳秒为单位
装饰可以超越或包含在统一的日志输出中。无论使用哪种装饰,它们都将按以下顺序出现在输出中:
timeuptimetimemillisuptimemillistimenanosuptimenanospidtidleveltags
级别
记录的消息单独与详细级别相关联。如前所述,级别为错误、警告、信息、调试、跟踪、开发。下表显示了这些级别相对于记录的信息量的详细程度是如何增加的。“开发级别”仅用于开发目的,在产品应用内部版本中不可用:

日志消息的详细级别
使用 Xlog 输出
Java 日志框架支持三种类型的输出,包括直接使用-Xlog命令行语法的示例:
在下面的示例中,我们向stderr提供输出:
-Xlog:all=warning:stderr:none
下面的示例向stdout提供输出:
-Xlog:all=warning:stdout:none
以下示例将输出写入文本文件:
-Xlog:all=warning:file=logmessages.txt:none
标签
新的日志框架由一组在 JVM 中标识的标记组成。如果需要,可以在源代码中更改这些标记。标签应该是自识别的,例如用于垃圾收集的gc。
当多个标记组合在一起时,它们形成一个标记集。当我们通过源代码添加自己的标记时,每个标记都应该与一个标记集相关联。这将有助于确保标签保持有序,并且易于人类阅读。
编译器控制
控制 Java 虚拟机(JVM)编译器似乎是一项不必要的任务,但对于许多开发人员来说,这是测试的一个重要方面。这是通过依赖于方法的编译器标志实现的。
在本节中,我们将从 JVM 编译模式开始,然后看看可以使用 Java 平台控制的编译器。
编译模式
现代 Java 平台的变化包括对 JVM 编译器的细粒度控制。如下图所示,Java HotSpot JVM 有两种 JIT 编译模式 C1 和 C2:

Java HotSpot JVM 编译模式
C1 和 C2 编译模式使用不同的编译技术,如果在同一个代码基上使用,可以产生不同的机器代码集。
C1 编译模式
Java HotSpot VM 中的 C1 编译模式通常用于具有以下特征的应用:
- 快速启动
- 增强优化
- 客户端
C2 编译模式
第二种编译模式 C2 由具有下列特征的应用使用:
- 长运行时间
- 服务器端
分层编译
分层编译允许我们同时使用 C1 和 C2 编译模式。从 Java8 开始,分层编译是默认的过程。如图所示,启动时使用 C1 模式有助于提供更大的优化。然后,一旦 App 充分预热,则采用 C2 模式:

分层编译
Java11 中的编译器控制
Java 承诺能够有限地控制 JVM 编译器并在运行时进行更改。这些额外的能力不会降低性能。这使得测试和测试优化更加逼真,因为我们可以运行小型编译器测试,而不必重新启动整个 JVM。
为了控制编译器操作,我们需要创建一个指令文件。这些文件包含由一组带有值的选项组成的编译器指令。指令文件基本上使用 JSON 的一个子集:

编译器指令结构
JavaScript 对象表示法(JSON)格式用于数据交换。指令文件与 JSON 有以下格式差异:
int和doubles是唯一支持的数字格式- 双正斜杠(
//)可用于注释行 - 尾随逗号(
,)可用于数组和对象中 - 不支持转义字符
- 选项名的格式为字符串,不必加引号
您可以在这个页面了解更多关于 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
诊断命令
在现代 Java 平台中添加了七个新的诊断命令,以增强诊断 JDK 和 JVM 的能力。
print_codegenlist命令打印当前排队等待编译的方法。由于 C1 和 C2 编译模式位于不同的队列上,因此需要向特定队列发出此命令。
dump_codelist诊断命令将打印编译方法的下列信息:
- 完整签名
- 地址范围
- 状态:
- 活着
- 非参与
- 僵尸
此外,dump_codelist诊断命令允许将输出定向到stdout或指定的文件。输出可以是 XML 格式或标准文本。
print_codeblocks命令允许我们打印以下内容:
- 代码缓存大小
- 代码缓存列表
- 代码缓存中的块列表
- 代码块地址
datadump_request诊断命令向 Java 虚拟机工具接口(JVMTI)发送转储请求。它取代了 Java 虚拟机调试接口(JVMDI)和 Java 虚拟机评测接口(JVMPI)。
使用set_vmflag命令,我们可以在 JVM 或库中设置命令行标志或选项。
print_class_summary诊断命令打印所有加载类的列表及其继承结构。
print_utf8pool命令打印所有 UTF-8 字符串常量。
堆性能分析代理
JVMTI hprof代理最近从 Java 平台上删除了。以下是与此更改相关的关键术语:
- 工具接口(TI):这是一个本机编程接口,允许工具控制正在 Java 虚拟机内运行的应用的执行。该接口还允许状态查询。这个工具的完整术语是 Java 虚拟机工具接口,或 JVMTI。
- 堆性能测试(HPROF):这是一个内部 JDK 工具,用于分析 JVM 对 cpu 和堆的使用。开发人员最常见的暴露是崩溃后生成的文件。生成的文件包含堆转储。
Java11JDK 不包含hprof代理。它被删除主要是因为有更好的替代品可用。以下是它们的相关功能表:
| HPROF 功能 | 备选方案 |
|---|---|
| 分配探查器(堆=站点) | Java 可视化 |
| CPU 档案器(CPU=样本) | Java VisualVM |
| (CPU=次数) | Java 飞行记录器 |
| 堆转储(Heap=dump) | 内部 JVM 功能: |
GC.heap_dump(icmd <pid> GC.heap_dump) |
|
jmap -dump |
有趣的是,最初创建 HPROF 时,并不打算在生产中使用它。实际上,它只是为了测试 JVM 工具接口的代码。因此,随着现代 Java 平台的出现,HPROF 库(libhprof.so将不再是 JDK 的一部分。
移除 JHAT
Java 堆分析工具(JHAT)用于解析 Java 堆转储文件。此堆转储文件解析工具的语法如下:
jhat
[-stack <bool>]
[-refs <bool>]
[-port <port>]
[-baseline <file>]
[-debug <int>]
[-version]
[-h|-help]
<file>
下面简要介绍与 JHAT 命令相关的选项:
| 选项 | 说明 | 默认值 |
|---|---|---|
-J<flag> |
这会将<flag>传递给运行时系统 |
不适用 |
-stack<bool> |
这将切换对象分配调用栈的跟踪 | true |
-refs<bool> |
这将切换对对象引用的跟踪 | true |
-port<port> |
这表示 JHAT HTTP 服务器的端口 | 7000 |
-exclude<exclude-filename> |
这将从可访问对象中排除指定的文件 | 不适用 |
-baseline<filename> |
这将指定用于比较的基准堆转储 | 不适用 |
-debug<int> |
这将设置输出的详细程度 | 不适用 |
-version |
这只是输出 JHAT 版本号 | 不适用 |
-h -help |
这将提供帮助文本 | 不适用 |
JHAT 从 JDK-6 开始就以实验的形式成为 Java 平台的一部分。它不受支持,被认为是过时的。从 Java9 开始,这个工具不再是 JDK 的一部分。
命令行标志参数验证
在本章中,您已经了解了 Java 平台中命令行标志的许多用法。一致努力确保所有带参数的 JVM 命令行标志都得到验证。这项工作的主要目标是:
- 避免 JVM 崩溃
- 提供错误消息来告诉你无效的标志参数
从下图中可以看到,没有尝试自动更正标志参数错误;相反,只是为了识别错误并防止 JVM 崩溃:

标志参数错误
这里提供了一个示例错误消息,指出标志参数超出范围。此错误将在 JVM 初始化期间执行的标志参数范围检查期间显示:
exampleFlag UnguardOnExecutionViolation = 4 is outside the allowed range [0 . . . 3]
以下是一些有关 Java 平台更改的细节:
-
展开当前的
globals.hpp源文件,以确保完整的标志默认值和允许的范围被记录 -
定义一个框架以支持将来添加新的 JVM 命令行标志:
-
这将包括值范围和值集
-
这将确保有效性检查将应用于所有新添加的命令行标志
-
修改宏表:
-
为可选范围添加最小值/最大值
-
为以下项添加约束项:
- 确保每次标记更改时都执行约束检查
- 当 JVM 运行时,将继续检查所有可管理的标志
为旧平台版本编译
Java 编译器javac在 Java9 中进行了更新,以确保它可以用来编译 Java 程序,以便在用户选择的旧版本 Java 平台上运行。在下面的截图中可以看到,javac有几个选项,包括-source和-target。以下截图中显示的javac来自 Java8:

Java8 中的 Javac 选项
-source选项用于指定编译器接受的 Java 版本。-target选项通知您将生成哪个版本的类文件javac。默认情况下,javac生成最新 Java 版本和平台 API 版本的类文件。当编译的应用使用仅在最新平台版本中可用的 API 时,这可能会导致问题。这将导致应用无法在较旧的平台版本上运行,尽管使用了-source和-target选项。
为了解决上述问题,Java 中提供了一个新的命令行选项,这个选项是--release选项,当使用这个选项时,会自动配置javac来生成与特定平台版本相链接的类文件。下面的屏幕截图显示了当前 Java 平台的javac选项。如您所见,新的--release选项包括:

Java18.9 中的 Javac 选项
以下是新选项的语法:
javac --release <release> <source files>
基于 Java 的实验性 JIT 编译器
在 Java10 中启用了基于 Java 的即时(JIT)编译器,可以作为 Linux/x64 平台的实验性 JIT 编译器。基于 Java 的 JIT 编译器被称为 Graal
做出这一更改的目的是希望通过实验可以证明将 JIT 编译器添加到 JDK 中的概念
总结
在本章中,我们探讨了现代 Java 平台的一些变化,这些变化的共同主题是命令行标志。具体来说,我们讨论了统一 JVM 日志记录、编译器控制、新的诊断命令、HPROF 堆分析代理的删除、JHAT 的删除、命令行标志参数验证,以及针对旧平台版本进行编译的能力。
在下一章中,我们将重点介绍 Java 中提供的附加工具的最佳实践。其中包括 UTF-8、Unicode 7.0、Linux 等等。
问题
-
Java9 中引入的 JVM 日志记录模式是什么?
-
日志记录的五个类别是什么?
-
什么是装饰?
-
日志中的详细程度是多少?
-
哪一个详细级别是最高的?
-
哪种详细程度最低?
-
如何更改日志标记?
-
什么用于控制 JVM?
-
Java HotSpot JVM 的 JIT 编译模式是什么?
-
哪种编译模式具有快速启动功能?
进一步阅读
以下是您可以参考的信息列表:
- 《Java SE 9 整洁代码入门》【视频】在这个页面提供。
十五、Java 平台的其他增强功能
在最后一章中,我们探讨了 Java 中命令行标志的一些变化,具体包括统一 JVM 日志记录、编译器控制、新的诊断命令、删除 HPROF 堆分析代理、删除 Java 堆分析工具(JHAT),命令行标志参数验证,以及为旧平台版本编译的能力。
在本章中,我们将重点介绍 Java 平台提供的附加工具的最佳实践。具体来说,我们将讨论以下主题:
- 支持 UTF-8
- Unicode 支持
- Linux/AArch64 端口
- 多分辨率图像
- 公共场所数据库(CDLR)
技术要求
本章及后续章节主要介绍 Java11,Java 平台的标准版(SE)可从 Oracle 官方下载网站下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
UTF-8 支持
Unicode 转换格式 8(UTF-8)是一个字符集,它封装了所有 Unicode 字符,使用一到四个 8 位字节。UTF-8 是面向字节的 Unicode 编码格式,自 2009 年以来一直是网页编码的主要字符集。
以下是 UTF-8 的一些特点:
- 它可以对所有 1112064 个 Unicode 代码点进行编码
- 它使用 1 到 4 个 8 位字节
- 它几乎占所有网页的 90%
- 它与 ASCII 向后兼容
- 它是可逆的
UTF-8 的广泛使用强调了确保 Java 平台完全支持 UTF-8 的重要性。对于 Java 应用,我们能够指定具有 UTF-8 编码的属性文件。Java 平台包括对ResourceBundleAPI 的更改,以支持 UTF-8。
让我们看一看前现代 Java(Java8 和更早版本)ResourceBundle类,然后看看在现代 Java 平台上对这个类做了哪些更改。
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_CLASSpublic static final List<String> FORMAT_DEFAULTpublic static final List<String> FORMAT_PROPERTIESpublic static final long TTL_DONT_CACHEpublic static final long TTL_NO_EXPIRATION_CONTROL
该类有一个空构造器和以下方法:
getCandidateLocales():
public List<Locale> getCandidateLocales(String baseName, Locale locale)
我们来看看getCandidateLocales()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | NullPointerException(如果baseName或locale为空) |
| 参数 | baseName:完全限定类名 |
locale:期望的locale |
|
| 返回 | 候选区域设置列表 |
getControl():
public static final ResourceBundle.Control getControl(List<String> formats)
我们来看看getControl()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | IllegalArgumentException(如果formats未知) |
NullPointerException(如果formats为空) |
|
| 参数 | formats:这些是ResourceBundle.Control.getFormats方法返回的格式 |
| 返回 | 支持指定格式的ResourceBundle.Control |
getFallbackLocale():
public Locale getFallbackLocale(String baseName, Locale locale)
我们来看看getFallbackLocale()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | NullPointerException(如果baseName或locale为空) |
| 参数 | baseName:完全限定类名 |
locale:ResourceBundle.getBundle方法找不到的期望的locale |
|
| 返回 | 后备locale |
getFormats():
public List<String> getFormats(String baseName)
我们来看看getFormats()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | NullPointerException(如果baseName为空) |
| 参数 | baseName:完全限定类名 |
| 返回 | 字符串列表及其格式,以便可以加载资源包 |
getNoFallbackControl():
public static final ResourceBundle.Control getNoFallbackControl(List<String> formats)
我们来看看getNoFallbackControl()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | IllegalArgumentException(如果formats未知) |
NullPointerException(如果formats为空) |
|
| 参数 | formats:这些是ResourceBundle.Control.getFormats方法返回的格式 |
| 返回 | 支持指定的格式的ResourceBundle.Control,没有后备locale。 |
getTimeToLive():
public long getTimeToLive(String baseName, Locale locale)
我们来看看getTimeToLive()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | NullPointerException(如果baseName为空) |
| 参数 | baseName:完全限定的类名 |
locale:时间locale |
|
| 返回 | 距离缓存时间的偏移,零或正毫秒 |
needsReload():
public boolean needsReload(String baseName, Locale locale, String format, ClassLoader loader, ResourceBundle bundle, long loadTime)
我们来看看needsReload()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | NullPointerException(如果下列任何参数为空): |
baseName |
|
locale |
|
format |
|
loader |
|
bundle |
|
| 参数 | baseName:完全限定类名 |
locale:期望的locale |
|
format:资源包格式 |
|
loader:用于加载包的ClassLoader |
|
bundle:过期包 |
|
ClassLoader loadTime:包被添加到缓存中的时间 |
|
| 返回 | true/false表示到期包是否需要重新加载 |
newBundle():
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
我们来看看newBundle()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | ClassCastException(如果被加载类不能转换为ResourceBundle) |
ExceptionInInitializerError(如果初始化失败) |
|
IllegalAccessException(如果构造器不可访问) |
|
IllegalArgumentException(如果格式未知) |
|
InstantiationException(如果类实例化失败) |
|
IOException(资源读取错误) |
|
NullPointerException(如下列参数空): |
|
baseName、locale、format |
|
SecurityException(如拒绝访问实例) |
|
| 参数 | |
baseName:完全限定类名 |
|
locale:所需的语言环境 |
|
format:资源包格式 |
|
loader:用于加载包的ClassLoader |
|
reload:true/false标志、指示资源包是否已过期 |
|
| 返回 | 资源包的实例 |
toBundleName():
public String toBundleName(String baseName, Locale locale)
我们来看看toBundleName()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | NullPointerException(如果baseName或locale为空) |
| 参数 | baseName:完全限定类名 |
locale:期望的locale |
|
| 返回 | 资源包名称 |
toResourceName():
public final String toResourceName(String bundleName, String suffix)
我们来看看toResourceName()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | NullPointerException(如果bundleName或suffix为空) |
| 参数 | bundleName:包名称 |
suffix:文件名后缀 |
|
| 返回 | 转换后的资源名称 |
字段和构造器
ResourceBundle类有一个字段,如下所述:
protected Resourcebundle parent
当找不到指定的资源时,通过getObject方法搜索父包。
ResourceBundle类的构造器如下:
public ResourceBundle() {
}
方法
ResourceBundle类有 18 个方法,这里分别描述:
clearCache():
public static final void clearCache()
从下表可以看出,clearCache()方法不抛出任何异常,不接受任何参数,也没有返回值:
| 组件 | 明细 |
|---|---|
| 抛出 | 没有 |
| 参数 | 没有 |
| 返回 | 没有 |
以下是以ClassLoader为参数的clearCache()方法的一个版本:
public static final void clearCache(ClassLoader loader)
以下是以ClassLoader为参数的clearCache()方法版本的详细信息:
| 组件 | 明细 |
|---|---|
| 抛出 | NullPointerException(如果loader为空) |
| 参数 | loader:类的loader |
| 返回 | 没有 |
containsKey():
public boolean containsKey(String key)
我们来看看containsKey()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | NullPointerException(如果key为空) |
| 参数 | key:资源key |
| 返回 | true/false取决于key是在ResourceBundle还是在父束中 |
getBundle():
public static final ResourceBundle getBundle(String baseName)
我们来看看第一版getBundle()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | MissingResourceException |
NullPointerException(如果baseName为空) |
|
| 参数 | baseName:完全限定类名 |
| 返回 | 基于给定baseName和默认locale的资源包 |
以下是第二版getBundle()方法的语法:
public static final ResourceBundle getBundle(String baseName, Resourcebundle.Control control)
我们来看看第二版getBundle()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | IllegalArgumentException(如果传递的control执行不当) |
MissingResourceException(如果所提供的baseName的资源包找不到) |
|
NullPointerException(如果baseName为空) |
|
| 参数 | baseName:完全限定类名 |
control:提供信息、以便加载resource包 |
|
| 返回 | 基于给定baseName和默认locale的资源包 |
以下是第三版getBundle()方法的语法:
public static final ResourceBundle getBundle(String baseName, Locale locale)
我们来看看第三版getBundle()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | MissingResourceException |
NullPointerException(如或baseName或locale为空) |
|
| 参数 | baseName:完全限定类名 |
locale:期望的locale |
|
| 返回 | 基于给定的baseName和locale的资源包 |
以下是第四版getBundle()方法的语法:
public static final ResourceBundle getBundle(String baseName, Locale targetLocale, Resourcebundle.Control control)
我们来看看第四版getBundle()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | IllegalArgumentException(如果传递的control执行不当) |
MissingResourceException(如果所提供的baseName的资源包找不到) |
|
NullPointerException(如果baseName、control或locale为空) |
|
| 参数 | baseName:完全限定类名 |
control:提供信息、以便加载resource包 |
|
targetLocale:期望的locale |
|
| 返回 | 基于给定的baseName和locale的资源包 |
以下是第五版getBundle()方法的语法:
public static final ResourceBundle getBundle(String baseName, Locale locale, ClassLoader loader)
我们来看看第五版getBundle()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | MissingResourceException(如果所提供的baseName的资源包找不到) |
NullPointerException(如果baseName、control或locale为空) |
|
| 参数 | baseName:完全限定类名 |
locale:期望的locale |
|
loader:类的loader |
|
| 返回 | 基于给定的baseName和locale的资源包 |
以下是第六版getBundle()方法的语法:
public static final ResourceBundle getBundle(String baseName, Locale targetLocale, ClassLoader loader, ResourceBundle.Control control)
我们来看看第六版getBundle()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | IllegalArgumentException(如果传递的control执行不当) |
MissingResourceException(如果所提供的baseName的资源包找不到) |
|
NullPointerException(如果baseName、control或locale为空) |
|
| 参数 | baseName:完全限定类名 |
control:提供信息、以便加载resource包 |
|
locale:期望的locale |
|
loader:类的loader |
|
| 返回 | 基于给定的baseName和locale的资源包 |
getKeys():
public abstract Enumeration<String> getKeys()
我们来看看Enumeration()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | 没有 |
| 参数 | 没有 |
| 返回 | ResourceBundle和父包中的键的枚举 |
getLocale():
public Locale getLocale()
我们来看看getLocale()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | 没有 |
| 参数 | 没有 |
| 返回 | 当前资源包的locale |
getObject():
public final Object getObject(String key)
我们来看看getObject()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | MissingResourceException(如果所提供的baseName的资源包找不到) |
NullPointerException(如果baseName、control或locale为空) |
|
| 参数 | key:这是所需对象的key |
| 返回 | 提供key的对象 |
getString():
public final String getString(String key)
我们来看看getString()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | ClassCastException(如果被加载类不能转换为ResourceBundle) |
MissingResourceException(如果所提供的baseName的资源包找不到) |
|
NullPointerException(如果baseName、control或locale为空) |
|
| 参数 | key:这是所需String的关键 |
| 返回 | String提供的键 |
getStringArray():
public final String[] getStringArray(String key)
我们来看看getStringArray()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | IllegalArgumentException(如果传递的control执行不当) |
MissingResourceException(如果所提供的baseName的资源包找不到) |
|
NullPointerException(如果baseName、control或locale为空) |
|
| 参数 | key:这是所需String数组的key |
| 返回 | 为key提供String数组 |
handleGetObject():
protected abstract Object handleGetObject(String key)
我们来看看handleGetObject()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | NullPointerException(如果key为空) |
| 参数 | key:key表示所需的Object |
| 返回 | 给定key的对象 |
handleKeySet():
protected Set<String> handleKeySet()
我们来看看handleKeySet()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | 没有 |
| 参数 | 没有 |
| 返回 | ResourceBundle中的一组键 |
keySet():
public Set<String> keySet()
我们来看看keySet()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | 没有 |
| 参数 | 没有 |
| 返回 | ResourceBundle及其parent包中的一组键 |
setParent():
protected void setParent(ResourceBundle parent)
我们来看看setParent()方法的细节:
| 组件 | 明细 |
|---|---|
| 抛出 | 没有 |
| 参数 | parent:当前捆绑的parent捆绑 |
| 返回 | 没有 |
现代 Java 平台的变化
Java 平台以前支持基于 ISO-8859-1 的属性文件格式。这种格式不容易支持转义字符,尽管它提供了适当的转义机制。使用 ISO-8859-1 需要在文本字符与其转义形式之间进行转换。
当前的 Java 平台包括一个修改过的ResourceBundle类,其默认文件编码设置为 UTF-8,而不是 ISO-8859-1。这节省了应用进行上述转义机制转换所需的时间。
Unicode 支持
随着 Unicode 规范的更新,Java 平台也随之更新。Java8 支持 Unicode 6.2,Java9 支持 Unicode 7.0,Java11 支持 Unicode 10.0.0,于 2017 年 6 月 20 日发布。
有关 Unicode 版本 10.0.0 的更多信息,请访问官方规范页面。
Java 平台尚未实现以下 Unicode 标准:
- Unicode 技术标准 #10(UTS#10):Unicode 排序算法详细说明了如何比较 Unicode 字符串
- Unicode 技术标准 #39(UTS#39):Unicode 安全机制
- Unicode 技术标准 #46(UTS#46):Unicode 应用中的国际化域名(IDNA)——允许应用使用 ASCII 字符串标签来表示非 ASCII 标签
- Unicode 技术标准 #51(UTS#51):Unicode 表情符号
特定于 Unicode 支持的核心 Java 平台更改包括以下 Java 类:
-
java.lang包包括以下内容:CharacterString
-
java.text包包括以下内容:BidiBreakIteratorNormalizer
让我们快速看一下这些类中的每一个,以帮助巩固我们对 Unicode 10.0.0 在 Java 平台上的广泛影响的理解。
java.lang包
java.lang包提供了几乎所有 Java 应用中使用的基本类。在本节中,我们将介绍Character和String类。
这是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
字符串(字符串是另一个核心原始类)是不可变的字符串。
修改Character和String类以支持更新的 Unicode 版本,即用于 Java9 和更高版本的版本 7.0,这是帮助保持 Java 作为首要编程语言的相关性的一个重要步骤。
java.text包
Bidi、BreakIterator和Normalizer类的应用不如Character和String类广泛。以下是这些类的简要概述:
这是Bidi类:
public final class Bidi extends Object
此类用于实现 Unicode 的双向算法。用于支持阿拉伯语或希伯来语。
有关 UNICODE 双向算法的具体信息,请访问这个页面。
BreakIterator类用于查找文本边界:
public abstract class BreakIterator extends Object implements Cloneable
这是Normalizer类:
public final class Normalizer extends Object
此类包含两个方法:
isNormalized:用于确定给定序列的char值是否归一化normalize:规范化char值的序列
额外重要事项
如前所述,JDK8 支持 Unicode 6.2。6.3 版于 2013 年 9 月 30 日发布,主要内容如下:
- 双向行为改进
- 改进的 Unihan 数据
- 更好地支持希伯来语
2014 年 6 月 16 日发布的 7.0.0 版引入了以下更改:
- 添加了 2834 个字符
- 增加对阿塞拜疆语、俄语和高级德语方言的支持
- 象形符号
- 多个国家和地区的历史脚本
- Unicode 双向算法的更新。
- 新增粤语发音词条近 3000 条。
- Indic 脚本属性的主要增强。
Unicode 在 6.3 和 7.0.0 版本中的巨大变化强调了当前支持 7.0.0 的 Java 平台的重要性,而不是像 Java8 那样支持 6.3。
Linux/AArch64 端口
从 JDK9 开始,JDK 已经被移植到 Linux/AArch64。为了理解这对我们 Java 开发人员意味着什么,让我们来讨论一下硬件。
ARM 是一家英国公司,30 多年来一直在开发计算核心和架构。他们的原名是 Acorn RISC Machine,RISC 代表精简指令集计算机。在此过程中,公司更名为高级 RISC 机器(ARM),最后更名为 ARM Holdings,或者干脆更名为 ARM。它将其架构授权给其他公司。ARM 报告说,已经制造了超过 1000 亿个 ARM 处理器。
2011 年末,ARM 推出了一个新的 ARM 架构,名为 ARMv8。这个架构包括一个名为 AArch64 的 64 位可选架构,正如您所料,它附带了一个新的指令集。以下是 AArch64 功能的简要列表:
- A64 指令集:
- 31 个通用 64 位寄存器
- 专用零或栈指针寄存器
- 接受 32 位或 64 位参数的能力
- 高级 SIMD(NEON)-增强:
- 32 x 128 位寄存器
- 支持双精度浮点
- AES 加密/解密和 SHA-1/SHA-2 哈希
- 新的异常系统
Oracle 在确定这种架构是现代 Java 平台需要支持的方面做了大量的工作。据说新的 AArch64 架构本质上是一种全新的设计。JDK9、10 和 11 已通过以下实现成功移植到 Linux/AArch64:
- 模板解释器
- C1 JIT 编译器
- C2 JIT 编译器
有关 C1 和 C2 JIT 编译器的信息,请参阅第 14 章“命令行标志”。
多分辨率图像
Java11 包含一个支持多分辨率图像的 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 包含分别返回图像和图像列表的getResolutionVariant和getResolutionVariants。因为MultiResolutionImage是一个接口,所以我们需要一个抽象类来实现它。
通用区域数据仓库
默认情况下,Java11 实现了使用 Unicode 公共语言环境数据存储库中的语言环境数据的决策。CLDR 是许多支持多种语言的软件应用的关键组件,它被吹捧为最大的语言环境数据存储库,被众多大型软件供应商使用,包括苹果、谷歌、IBM 和微软。CLDR 的广泛使用使其成为非官方的行业标准语言环境数据存储库。使其成为当前 Java 平台中的默认存储库进一步巩固了其作为软件行业标准的地位。
有趣的是,CLDR 已经是 JDK8 的一部分,但不是默认库。在 Java8 中,我们必须通过设置系统属性来启用 CLDR,如下所示:
java.locale.providers=JRE,CLDR
现在,在 Java 中,我们不再需要启用 CLDR,因为它将是默认的存储库
在当前的 Java 平台中还有其他语言环境数据存储库。它们按默认的查找顺序列在此处:
- CLDR
- COMPAT(以前叫 JRE)
- 服务提供商接口(SPI)
要更改查找顺序,我们可以更改java.locale.providers设置,如图所示:
java.locale.providers=SPI,COMPAT,CLDR
在上例中,SPI将首先,然后是COMPAT,然后是CLDR。
总结
在本章中,我们将重点介绍当前 Java 平台提供的附加工具的最佳实践。具体来说,我们介绍了 UTF-8 属性文件、Unicode 7.0.0、Linux/AArch64 端口、多分辨率图像和公共语言环境数据存储库。
在下一章中,我们将通过展望 Java19.3(Java12)和 Java19.9(Java13)中的内容来展望 Java 平台的未来方向。
问题
- 什么是 UTF-8?
- 列出 UTF-8 的五个特性。
- 哪个类为开发人员提供了从资源包中隔离特定于语言环境的资源的能力?
clearCache()方法返回什么?getBundle()方法返回什么?- Java11 支持什么版本的 Unicode?
- JDK9、10 和 11 已经成功移植到 Linux/AArch64。列出三种实现。
- 什么是多分辨率图像?
- 哪个类用于从多分辨率图像中检索所需的变体?
- 什么是 CLDR?
十六、未来发展方向
在最后一章中,我们重点介绍了 Java 平台提供的一些激动人心的工具的最佳实践。具体来说,我们介绍了 UTF-8 属性文件、Unicode、Linux/AArch64 端口、多分辨率图像和公共区域设置数据存储库。
本章概述了 Java 平台在 Java11 之外的未来发展。我们将看看 Java19.3(12)和 19.9(13)的计划以及将来可能看到的进一步变化。我们将从一个简短的 JEP 概述开始。
具体而言,本章包括以下内容:
- JEP 概述
- 候选的 JEP
- 提交的 JEP
- 起草的 JEP
- 正在进行的特别项目
技术要求
本章及后续章节主要介绍 Java11。Java 平台的标准版(SE)可从 Oracle 官方下载网站下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章和后续章节相关的所有编码。IntelliJ IDEA 的社区版可从网站下载。
JDK 增强提案概述
JDK 增强提案(JEP)由 JDK 的建议变更列表组成。这个公开的列表用于通知开发人员并为 Java 平台提供一个长期计划
JEP 的核心是增强功能本身。增强的标准包括:
- 必须表明 JDK 发生了重大变化。
- 必须有广泛的信息吸引力。
- 此外,它必须满足以下附加标准之一:
- 需要大量的工程工作来实现(至少两周)
- 涉及对 JDK 或其基础结构的重大更改
- 是 Java 开发人员或用户的高需求项目
JEP 具有以下顺序状态:
- 草案:这是最早的状态,当 JEP 已经写好,作者正在传阅征求共识时使用
- 发布:一旦作者达成共识,JEP 即可登录 JEP 档案
- 提交:一旦 JEP 被认为准备好进行正式评估,作者即设置此状态
- 候选:一旦 OpenJDK 负责人接受了 JEP,就变为这个状态
- 资助:资助是指某个职能部门领导或某个小组领导判断 JEP 资金充足
- 已完成:此状态表示增强工作已完成并在版本发布中交付
并不是每个 JEP 都能完成整个六阶段的工作流程。其他状态包括激活、拒绝、撤销。
Java 平台的每一个潜在更改都将被描述为有针对性的、提交的或起草的。有针对性是指已指定用于未来 Java 版本的更改,已提交是指已提交但未针对 Java 平台特定版本的更改,并且起草的更改仍在绘图板上,尚未准备好提交或指定为有针对性的更改。
下一节将介绍 JDK 增强方案,包括候选、已提交和草稿的状态。
JEP 候选
这一部分的特点是五个 JEP,在写这本书的时候,有候选人的地位。这些 JEP 如下:
- JEP326:原始字符串字面值
- JEP334:JVM 常量 API
- JEP337:RDMA 网络套接字
- JEP338:向量 API
- JEP339:Edwards 曲线数字签名算法
JEP326:原始字符串字面值
在未来的版本中,我们可能会看到 Java 平台中添加了原始字符串字面值。原始字符串字面值有两个重要特性:
- 它们可以跨越多行源代码
- 转义序列不被解释
这个增强的目标本质上是让开发人员更容易。此更改将允许开发人员提供包含多行代码的字符串,而不必提供字符来指示新行。
JEP334:JVM 常量 API
JVM 依赖于一个常量池来确定类布局、实例、接口和数组。每个类都有一个固定的池。当前用于建模这些可加载常量的 Java 数据类型效率低下且不充分。这个 JEP 的目的是为 Java 提供操纵类和方法的能力。
JEP337:RDMA 网络套接字
远程直接内存访问(RDMA)允许一台计算机访问另一台计算机的内存,而无需通过操作系统。这个 JEP 希望将 RDMA 添加到 JDK 网络 API 中。至少在最初,这将适用于 Linux 系统。
JEP338:向量 API
将创建一个新的向量 API,以便向量计算可以可靠地编译成针对特定 CPU 优化的向量硬件指令。这将使开发人员能够提供高效的标量计算。
JEP339:Edwards 曲线数字签名算法
Edwards 曲线数字签名算法(EdDSA)是一种密码签名。这个 JEP 计划实现这个算法。
已提交的 JEP
在撰写本书时,以下 JEP 的状态为提交:
- 默认情况下,将禁用实验功能
- 默认的 CDS 存档将包含在 JDK 二进制文件中
- Javadoc 标签的创建是为了区分 API、实现、注释和规范之间的差异
- 新的 JMX 注解将用于注册托管资源
- GTK3 将在布局方面进行现代化改造
- 将为 JMX 创建新的 REST API
每项建议的详细信息可从这个页面获得。
已起草的 JEP
在撰写本书时,以下 JEP 的起草状态为:
- G1 的可中止混合集合
- Java 堆在备用内存设备上的旧一代的分配
- 更好的哈希码
- 并行监视器
- 动态最大内存限制
- 高效数组比较
- 在 GPU 上启用 Java 方法的执行
- 使用值类型增强的
ManderblotSet演示 - 增强型伪随机数生成器
- 改进的 IPv6 支持
- 孤立的方法
- Java 线程清理器
- 应用启动时预编译的
JWarmupJava 热方法 - 密钥派生 API
- 包装工具
- 在 JVM 编译的方法上提供稳定的 USDT 探测点
- 支持映射到非易失性存储器上的
ByteBuffer - 及时减少未使用的已提交内存
- JVM 中的类型运算符表达式
- 方法句柄的未装箱参数列表
- 使用 UTF-8 作为默认字符集
每项建议的详细信息可从这个页面获得。
正在进行的特别项目
Java 增强建议展示了 Java 平台的设计和实现变化。如前所述,正在起草的 JEP 的标准是,工作必须至少满足以下一项:
- 需要大量的工程工作来实现(至少两周)
- 涉及对 JDK 或其基础结构的重大更改
- 是 Java 开发人员或用户的高需求项目
另一方面,项目代表由下列小组之一赞助的合作努力:
- 二维图形
- 采用
- 抽象窗口工具箱(AWT)
- 建造
- 兼容性和规范审查
- 编译器
- 一致性
- 核心库
- 理事会
- HotSpot
- 国际化
- JMX
- 成员
- 网络
- NetBeans 项目
- 搬运工
- 质量
- 安全
- 适用性
- 声音
- Swing
- 漏洞
- Web
小组是正式的,可以提出新的小组。
下面列出的活动项目代表了 Java 平台未来可能的增强领域。本节后面将提供有关每个项目的简要信息,并深入了解未来变化的一般领域:
- 注解管道 2.0
- 音频合成引擎
- Caciocavallo
- 通用虚拟机接口
- 编译器语法
- 设备 I/O
- Graal
- HarfBuzz 集成
- Kona
- 开放 JFX
- Panama
- Shenandoah
注解管道 2.0
这个项目探索如何在 Java 编译器管道中处理注解的改进。我们无意提议更改规范,而是将重点放在性能增强上。
音频合成引擎
这个项目正在考虑为 JDK 创建一个新的 MIDI 合成器。当前的 MIDI 合成器属于授权库。工作组希望看到新的 MIDI 合成器作为一个开源 JDK 素材。
Caciocavallo
Caciocavallo 项目旨在改进 OpenJDK AWT 内部接口。这扩展到二维子系统。提议的改进将简化 AWT 移植到新平台的方式。
通用虚拟机接口
公共 VM 接口项目的目标是为 OpenJDK 记录 VM 接口。这将使 Classpath VM 和其他 VM 更容易使用 OpenJDK。
编译器语法
编译器语法项目正在开发一个基于 ANTLR 语法的实验性 Java 编译器。另一种语言识别工具(简称 ANTLR),是一种读取、处理和执行结构化文本或二进制文件的解析器。项目组希望这个 Java 编译器能够取代当前的编译器,因为它使用的是手写解析器 LALR(缩写为 Look Ahead Left to Right)。LALR 解析器已经被项目组确认为脆弱的,难以扩展。
设备 I/O
这个项目打算通过 Java 级 API 提供对通用外围设备的访问。项目组希望支持的外围设备的初始列表包括:
- GPIO(简称通用输入/输出)
- I2C(简称集成电路间总线)
- SPI(简称串口)
- UART(简称通用异步收发机)
Graal
Graal 项目的目标是通过 JavaAPI 公开 VM 功能。这将允许开发人员用 Java 为给定的语言运行时编写动态编译器,包括开发多语言解释器框架。
HarfBuzz 集成
HarfBuzz 集成项目希望将 HarfBuzz 布局引擎集成到 Java 开发工具包中。这是为了用 HarfBuzz 布局引擎替换 ICU 布局引擎。ICU 布局引擎已被弃用,巩固了该项目未来成功的重要性。
Kona
Kona 项目正在定义和实现 Java API,以支持物联网(IoT)领域。这包括网络技术和协议。尽管没有说明,但安全和安保对于这项工作的成功实现至关重要。
开放 JFX
关于 OpenJFX 项目没有太多可用的细节。这个项目的既定目标是创建下一代 Java 客户端工具包。根据项目名称,可以假设团队希望创建 JavaFX 的 OpenJFX 版本,这是一组用于创建富互联网应用的包。
Panama
Panama 项目的重点是增强 JVM 和非 JavaAPI 之间的连接。
该项目包括以下选定组件:
- 本机函数调用
- 从 JVM 进行本机数据访问
- JVM 堆内的本机数据访问
- JVM 堆中的新数据布局
- 头文件的 API 提取工具
项目团队已经生成了一个与 JDK9 结构匹配的存储库树。这大大增加了项目成功的可能性。
Shenandoah
Shenandoah 项目的目标是通过垃圾收集操作显著减少暂停时间。这种方法是让更多的垃圾收集操作与 Java 应用同时运行。在第 7 章中,“利用默认的 G1 垃圾收集器”,您了解了 CMS 和 G1。Shenandoah 项目打算将并行压缩添加到可能的垃圾收集方法中。
总结
在这一章中,我们概述了 Java 平台在 Java11 之外的未来发展。Java 平台的每一个潜在变化都被描述为候选、提交或起草
在下一章中,您将学习如何为 Java 社区做出贡献以及这样做的好处。
问题
- 什么是 JEP?
- 一个提案必须有什么样的上诉才能为正义与平等党考虑?
- 提案必须满足的三个标准是什么?
- 在哪里可以找到活动 JEP 的列表?
- 起草的 JEP 状态意味着什么?
- 发布的 JEP 状态意味着什么?
- 提交的 JEP 状态意味着什么?
- 候选的 JEP 状态意味着什么?
- 受资助的 JEP 状态意味着什么?
- 完成的 JEP 状态意味着什么?
十七、为 Java 平台做贡献
在最后一章中,我们展望了 Java 平台在 Java11 之外的未来发展。我们查看了 Java19.3 和 19.9 的计划,以及将来可能会看到的进一步变化。我们首先简要概述了 JEP,并涵盖了现有的 JEP 和正在进行的特别项目。
Java 平台未来的关键是 Java 社区。这是本章的重点。我们将讨论 Java 社区以及开发人员如何为 Java 平台做出贡献。具体来说,我们将介绍以下与 Java 社区相关的主题:
- Java 社区
- 参与 Java 用户组
- Java 社区流程(JCP)
- 技术网络
- 撰写技术文章
技术要求
本章介绍 Java11,Java 平台的标准版(SE)可从 Oracle 官网下载。
IDE 包就足够了。来自 JetBrains 的 IntelliJ IDEA 用于与本章相关的所有编码。IntelliJ IDEA 的社区版可从以下网站下载。
Java 社区
Java 社区由数以百万计的开发人员组成,他们以一种或多种方式相互支持并为 Java 平台做出贡献。这就是 Java 是如此强大的开发平台的原因之一。社区参与可以包括通过@java关注和参与 Twitter,或者在 Facebook 上喜欢<3 Java。
还有大量的列表服务、博客和论坛,您可以参与其中,帮助开发人员解答问题。这是一个很好的方法来支持社区并及时了解 Java 平台的变化。并不是所有 Oracle 的博客都是针对 Java 的,但至少是 Java 的附属博客。
您可以在这个页面浏览 Oracle 的博客目录。
互联网上有几个 Java 论坛。甲骨文在这个页面开设了一个官方的 Java 社区空间。Java 社区空间组织为以下空间:
| JavaOne | Java 冠军 |
|---|---|
| #Java20 | Java 社区流程 |
| Java 本质论 | Java API |
| 数据库连接 | Java SE |
| Java 安全性 | Java HotSpot 虚拟机 |
| Java EE | 嵌入式技术 |
| Java 开发工具 | 文章档案 |
| Java.net Forge Sunset | JavaScript Nashorn |
| Java 新空间的构想 | Java 用户组 |
| NightHacking |
一些 Java 开发人员在 YouTube 上创建了 HOWTO 视频播放列表,其中许多被学术机构使用。这些可以是有趣的创造,是一个很好的方式回馈社会。
参与 Java 用户组
Java 用户组也被称为 JUGs,由具有社区意识的 Java 专业人士组成,旨在分享他们的 Java 知识。有超过 200 个用户组,参与是自愿的。与其他专业用户群一样,JUGs 提供以下机会:
- 与其他 Java 专业人士联网
- 分享技巧、技巧和资源
- 向他人学习
- 增加 Java 知识
用户组遍布全球。有些是按国家组织的,有些是按城市组织的。当你探索你所在地理区域的用户组时,你可能会发现他们有定期的见面会
Java 社区流程
Java 社区流程™(JCP™)是针对 Java 平台开发技术规范标准的结构化方法。所有开发者均可在这个页面注册。注册后,用户可以查看 Java 规范请求(JSR)并提交反馈
此外,您甚至可以提交自己的 JSR 建议并加入 JSR 专家组。
在 JCP 站点,您可以搜索和浏览当前 JSR。
技术网络
甲骨文拥有一个甲骨文技术网络(OTN),其成员资格是免费的。详情请访问这个页面。
OTN 会员有以下好处:
- 参加论坛的机会
- 自由软件
- 免费在线研讨会
- 访问 Java API 文档
- 社区参与
- 获取技术文章
- Oracle 员工的博客
- 访问示例代码
- 新闻稿订阅
- 活动、书籍、产品和其他资源的折扣
- 社会参与途径:
- 推特
- YouTube
- 脸书
- 博客
撰写技术文章
如果你是一个学者,专业人士,或只是想发表一篇文章,你可以提交给甲骨文。如果被接受,甲骨文将在 OTN 的保护下发布它们
以下网站提供了为 Oracle 撰写技术文章的详细信息。
本网站提供以下分类信息:
- 编辑需要
- 文章规范
- 提交和编辑过程
- 发表文章的授权
- 你的文章提交的回复
- 编辑过程
- 委托文章
总结
在本章中,我们讨论了 Java 社区以及可以为 Java 平台做出贡献的方法。具体来说,我们讨论了 Java 社区,参与了 Java 用户组 JCP 和 OTN,并为 Oracle 撰写了技术文章。
你做到了书的结尾。谢谢你的阅读。快乐的编码!
问题
- 什么是 JUG?
- 有多少个 JUG?
- JUG 是如何组织的?
- OTN 会员资格提供什么样的折扣?
- 什么是 Java 社区流程™?
- Java 社区有多大?
- 列出加入 JUG 的四个好处。
- 您可以从哪里了解到有关为 Oracle 编写技术文章的更多信息?
- 向 JSR 提交反馈的先决条件是什么?
- 谁可以提交 JSR?
十八、答案
第一章
- 19.3。
- 开发人员不再需要等待很长时间才能发布到 Java 平台。更重要的是,没有发布将代表平台的重大变化
- Java 平台的模块化。
- CORBA。
- 更快的启动。
- 低开销垃圾收集器。
- 标识符。
- Java18.3(10)。
- Java9、10(18.3)和 11(18.9)。
- G1。
第二章
- 当一个线程在一个队列中等待一个当前被锁定的对象时,它就被认为是在争夺这个锁
- 代码缓存是 Java 虚拟机(JVM)存储生成的本机代码的内存区域。
-XX:NonProfiledCodeHeapSize。- Lint 和 DocLint 是向
javac报告警告的来源。 hotspot/test/testlibrary/jit-tester。-Xshare:off。- FXML。
- 在 Java9 之前,字符串数据存储为一个数组
chars - 从 Java9 开始,字符串现在在内部使用字节数组和用于编码引用的标志字段来表示。
- OpenType 是一个 HTML 格式的字体格式规范。
第三章
- 在 Java 中,栅栏操作是
javac以屏障指令的形式对内存进行强制约束的操作。这些操作发生在屏障指令之前和之后,本质上是将它们封闭起来。 - Coin 项目是 Java7 中引入的一组小改动的特性集。
- 从 Java9 开始,我们可以对私有实例方法使用
@SafeVarargs注解。 - 从 Java9 开始,我们在类和文件中列出
import语句的顺序将不再影响编译过程。 - Java 平台在
cacerts密钥库中包含一组根证书。 var标识符在技术上是一个保留的类型名。- 可以通过使用新的
var标识符来推断声明。 - 下划线字符(
_)不能再用作合法的标识符名称。 java.util.concurrent.atomic包是 12 个子类的集合,支持对线程安全和无锁的单个变量执行操作。- 变量处理器是对变量的类型化引用,由
java.lang.invoke.VarHandle抽象类控制。
第四章
-
JDK、JRE 和 JAR 都太大了。
-
模块化系统具有以下要求:
- 必须有一个公共接口,以允许所有连接模块之间的互操作性
- 必须支持隔离和连接测试
- 编译时操作必须能够识别正在使用的模块
- 对模块的运行时支持
-
Java 模块是以下内容的集合:
- 包
- 类
- 接口
- 代码
- 数据
- 资源
-
java。 -
JDK 的主要组件如下:
- 开发工具
- JavaFX 工具
- Java 运行时环境(JRE)
- 源代码
- 库
- C 头文件
- 数据库
-
可维护性、性能和安全性。
-
bin、conf和lib。 -
bin、conf、lib、demo、sample、man、include。 -
链接时间。
-
Java 链接器。
第五章
-
当开发计算机上有多个版本的库时,库信息的特殊性不足,类加载器有问题,类路径过长。
-
Java9。
-
模块通过提供强大的封装解决了 Java9JDK 之前的单片问题。
-
java.base。 -
Java 中的封装是由
module-info.java文件中的信息驱动的。 -
jdk.unsupportedJDK 模块。 -
module-info.java。 -
Java 网络启动协议(JNLP)。
-
它是一个非法的标识符,从 Java10 开始。
-
--add-opens、--add-exports和--permit-illegal-access。
第六章
- 读取求值打印循环通常称为 REPL,从短语中的每个单词中提取第一个字母。它也被称为语言 Shell 或交互式顶层。
- 它是一个交互式读取求值打印循环工具,用于求值以下 Java 编程语言组件声明、语句和表达式。它有自己的 API,因此可以被外部应用使用。
- 如下所示:
- 制表符补全
- 语句结尾分号的自动补全
- 导入的自动补全
- 定义的自动补全
- JShell 是位于
/bin文件夹中的命令行工具。 - 退出 Shell 就像进入
/exit一样简单。 /vars。- 在 JShell 中输入
/help或/?命令提供了一个完整的命令列表和可以在 Shell 中使用的语法。 - 在 JShell 中,可以使用
/help命令,然后使用需要额外帮助的命令来获得额外的帮助。 - 命令行工具通常提供相对稀疏的反馈,以避免屏幕过度拥挤,或者对开发人员造成麻烦。JShell 有几种反馈模式,除了让开发人员能够创建自己的定制模式之外。
- 反馈方式有四种:
concise、normal、silent、verbose。
第七章
- 以下是垃圾收集算法:
- 标记和扫描
- CMS 垃圾收集
- 串行垃圾收集
- 并行垃圾收集
- G1 垃圾收集
-
G1 名称代表“垃圾优先”。
-
iCMS(增量并发标记扫描)、【CPU】、【37327】、【36739】、【35813】、
-
MiB 代表 Mebibyte,是数字信息字节的倍数。
-
尽管垃圾收集在 Java 中是自动的,但是您可以显式调用
方法java.lang.System.gc()方法来帮助调试过程。此方法不接受任何参数,也不返回任何值。 -
因为 Java 中的所有对象,甚至是您自己创建的对象,都是
java.lang.Object的子类,所以 Java 中的每个对象都有一个finalize()方法。 -
这些组合在 Java9 中已被删除。
-
CollectedHeap类。 -
对于 Java10,G1Full 垃圾收集器被转换为并行,以减轻对使用完全垃圾收集的开发人员的任何负面影响。
-
GC。
第八章
-
微基准是用来测试系统性能的。这与宏观基准测试不同,后者在不同的平台上运行测试,以进行效率比较和后续分析。使用微标记,我们通常针对一个系统上的特定代码片段,例如方法或循环。微基准的主要目的是在我们的代码中识别优化机会。
-
微基准发生在流程设计、实现、执行、分析和增强的几个阶段。
-
Maven,也称为 ApacheMaven,是一个项目管理和理解工具,我们可以使用它来管理我们的应用项目构建、报告和文档。
-
pom.xml。 -
它们都是可配置的选项。
-
All、AverageTime、SampleTime、SingleShotTime 和吞吐量。
-
纳秒,微秒,毫秒,秒,分钟,小时,天。
-
对于电源管理陷阱,有两种建议策略:
- 在运行测试之前禁用任何电源管理系统
- 长时间运行基准测试
- 对于操作系统调度器陷阱,有两种建议策略:
- 优化系统调度策略
- 长时间运行基准测试
- 对于分时陷阱,有两种建议策略:
- 在运行基准测试之前测试所有代码,以确保一切正常工作
- 只有当所有线程都已启动或停止时,才使用 JMH 进行测量
第九章
-
在 Java 应用编程的上下文中,进程是操作系统中的执行单元。当你启动一个程序,你就启动了一个过程。
-
Java9 中引入了两个支持处理操作系统进程的新接口:
ProcessHandle和ProcessHandle.Info。 -
处理完成后,
handle.is.Alive()方法返回false。 -
我们可以通过句柄访问进程的 PID。
handle.getPid()方法返回Long,表示 PID 的数值。 -
为了得到当前进程的 PID,可以使用调用链
ProcessHandle.current().getPid()。 -
command()、arguments()、commandLine()、startInstant()、totalCpuDuration()、user()。 -
为了得到控制子进程的进程句柄的
Stream,应该使用静态方法processHandle.children()。 -
processHandle.descendants()。 -
allProcess()。 -
CompletableFuture。
第十章
-
Java 运行时有一个名为
Stack的类,可以使用后进先出(LIFO)策略来存储对象。 -
JVM 是用 C 编写的,执行调用 C 函数并从那里返回的操作。
这个调用返回序列是使用本机方法栈维护的,就像其他 C 程序一样。 -
Java 虚拟机
Stack。 -
在 Java 中,我们使用 API 来获取
Logger。使用 API,模块可以为服务LoggerFinder提供实现,而服务LoggerFinder又可以返回实现getLogger()方法的Logger。 -
StackWalker类。 -
RETAIN_CLASS_REFERENCE、SHOW_REFLECT_FRAMES和SHOW_HIDDEN_FRAMES。 -
如果我们指定第一个选项枚举常量,
RETAIN_CLASS_REFERENCE作为getInstance()方法的参数,那么返回的实例将授予我们访问各个栈在遍历期间引用的类的权限。 -
SHOW_REFLECT_FRAMES枚举常量将生成一个遍历器,其中包含来自某个反射调用的帧。 -
枚举常量选项
SHOW_HIDDEN_FRAMES将包括所有隐藏帧,其中包含反射调用以及为 Lambda 函数调用生成的调用帧。 -
StackWalker类是最终类,不能扩展。
第十一章
-
易用性。
-
大约 10% 的 HTTP 协议未被 API 公开:
- 标准/通用认证机制仅限于基本认证
- 性能改进可能无法实现
- 不支持对请求进行过滤
- 不支持对响应进行过滤
- 新的 API 不包括可插入的连接缓存
- 缺乏通用的升级机制
-
包名、源文件名、访问控制选项。
-
languageVersion()、optionLength(String option)、start(RootDoc root)、validOptions(String[][] options, DocErrorReporter reporter)。 -
AttributeTree.ValueKind和DocTree.Kind。 -
HTML5。
-
JVM 的 JavaScript 引擎。
-
欧洲计算机制造商协会 JavaScript 脚本语言规范。
-
支持 JarFile API 和标准类加载器。
-
构造器不应接受任何参数。
* 它必须是可序列化的
* 它的属性必须包含更改器和访问器方法
第十二章
- 反应式编程是指应用在异步数据流发生时对其作出反应。
- 数据流是字符串和基本数据类型的二进制输入/输出。
- 效率。
- 以下是不需要的数据缓冲、数据转换和转换。
java.util.concurrent。Flow.Publisher、Flow.Subscriber、Flow.Subscription、Flow.Processor。- 多个进程共享一个处理器。
- 并发性和并行性经常被用作可互换的术语。并发是指当多个进程重叠时,尽管开始和停止时间可能不同。并行性发生在任务同时启动、运行和停止时。
- 共享内存系统配置有多个处理器,所有处理器共享一个公共系统内存。
- 在分布式存储系统中,每个处理器都有自己的内存,每个单独的处理器都与其他处理器完全链接,从而形成一个完全链接的分布式系统。
第十三章
-
数据报传输层安全(DTLS)是一种通信协议。
-
传输层安全(TLS)是最常见的网络安全协议。
-
在实现握手重新协商时,始终实现
renegotiation_info扩展。 -
实现 FS 可以确保过去的会话加密密钥不会在长期加密密钥受损时受损。
-
它本质上是存储公钥证书和私钥的数据库文件或数据存储库文件。
-
KeyStore将存储在/jre/lib/security/cacerts文件夹中。 -
KeyStore.Builder类用于延迟KeyStore的实例化。 -
它具有以下特点:
- 线程安全的
- 输入映射不需要同步
- 快速读取
- 使用锁来写入
- 无对象级锁定
- 在非常精细的级别上锁定
-
哈希码是一个对象生成的数字,存储在哈希表中,用于快速存储和检索。
-
Galois HASH(GHASH)是一种密码系统哈希算法。
第十四章
-
统一日志记录架构。
-
命令行选项、装饰、级别、输出和标签。
-
在 Java 日志框架的上下文中,装饰是关于日志消息的元数据。
-
错误、警告、信息、调试、跟踪和开发。
-
开发。
-
错误。
-
在源代码中。
-
依赖于方法的编译器标志。
-
C1 和 C2。
-
C1。
第十五章
-
Unicode 转换格式 -8(UTF-8)是一个字符集,它使用一到四个 8 位字节封装所有 Unicode 字符。它是 Unicode 的面向字节的编码形式。
-
以下是 UTF-8 的一些特点:
- 可以对所有 1112064 Unicode 代码点进行编码
- 使用一到四个 8 位字节
- 占所有网页的近 90%
- 向后兼容 ASCII
- 是可逆的
-
ResourceBundle类。 -
没有什么。
-
基于给定
baseName和默认locale的资源包 -
Java18.9(Java11)支持 Unicode 10.0.0,于 2017 年 6 月 20 日发布。
-
JDK9、10 和 11 已通过以下实现成功移植到 Linux/AArch64:
- 模板解释器
- C1 JIT 编译器
- C2 JIT 编译器
-
多分辨率图像包含同一图像的多个分辨率变体。
-
java.awt.Graphics类用于从多分辨率图像中检索所需的变量。 -
CLDR(简称公共区域数据仓库)是许多支持多种语言的软件应用的关键组件,被吹捧为最大的区域数据仓库,被众多大型软件供应商使用,包括苹果、谷歌、IBM 和微软。
第十六章
- JDK 的提议更改列表。
- 广泛的信息吸引力。
- 提案必须符合以下标准之一:
- 需要大量的工程工作来实现(至少两周)
- 涉及对 JDK 或其基础结构的重大更改
- 是 Java 开发人员或用户的高需求项目
- 草案这是最早的状态,在 JEP 已经写好并由作者分发以取得共识时使用。
- 一旦作者达成共识,JEP 就可以登录到 JEP 档案中。
- 一旦 JEP 被认为准备好进行正式评估,提交人就会设置这种状态。
- 一旦 OpenJDK 负责人接受了 JEP,它就变为这个状态。
- “受到资助”表示职能部门领导或集团领导已判断 JEP 获得全额资助。
- 已完成此状态表示增强工作已在版本发布中完成并交付。
- http://openjdk.java.net/jeps/0。
第十七章
- Java 用户组。
- 有 200 多个。
- 在地理上。
- 活动、书籍、产品和其他资源的折扣。
- Java 社区流程™是针对 Java 平台开发技术规范标准的结构化方法。
- Java 社区由数百万开发人员组成。
- 四大好处如下:
- 与其他 Java 专业人士联网
- 分享技巧、技巧和资源
- 向他人学习
- 增加 Java 知识


浙公网安备 33010602011771号