第 13 章 使用自定义运行时映像进行缩减

Chapter 13. Scaling Down with Custom Runtime Images

Now that you’ve seen all the tools and processes to work with modular applications, there’s one more exciting opportunity to explore. In “Linking Modules”, you got a taste of creating runtime images tailored to a specific application. Only the modules required to run the application become part of the image. A minimal runtime image can be automatically generated with jlink by using explicit dependency information available in modules.

到目前为止,已经学习了使用模块化应用程序所需的工具和流程,接下来可以进行进一步的探索。在 3.1.7 节中,介绍了如何为特定应用程序创建运行时映像。只有运行应用程序所需的模块才会成为映像的一部分。通过使用模块中的显式依赖信息,可以使用 jlink 自动生成最小的运行时映像。

Creating a custom runtime image is beneficial for several reasons:

创建自定义运行时映像有以下几个原因:

  • Ease of use: jlink delivers a self-contained distribution of your application and the JVM, ready to be shipped.
  • Reduced footprint: Only the modules that your application uses are linked into the runtime image.
  • Performance: A custom runtime potentially runs faster by virtue of link-time optimizations that are otherwise too costly or impossible.
  • Security: With only the minimum required platform modules in a custom runtime image, the attack surface decreases.

  • 易用性:jlink 提供了应用程序和 JVM 的一个独立分发。
  • 减少占用空间:只有应用程序使用的模块才会链接到运行时映像。
  • 性能:由于链接时优化的代价过高或不可能,因此自定义运行时可能会更快地运行。
  • 安全性:在自定义运行时映像中只需最少的平台模块,因此攻击面更小。

Even though creating a custom runtime image is an optional step, having a smaller binary distribution that runs faster is a compelling motivation—especially when applications target resource-constrained devices, such as embedded systems; or, when they run in the cloud, where everything is metered. Putting a custom runtime image into a Docker container is a good way to create resource-efficient cloud deployments.

尽管创建自定义运行时映像是一个可选步骤,但由此产生的运行速度更快的二进制分发是创建自定义运行时映像一个令人信服的动机——尤其是当应用程序面向资源受限设备(如嵌入式系统)时或者当它们在云端运行时(此时,一切都被计量)。将自定义运行时映像放入 Docker 容器是创建资源高效的云部署的好方法。

TIP

Other initiatives are improving Java’s support for containers. For example, OpenJDK 9 now also offers an Alpine Linux port. Running the JDK or a custom runtime image on top of this minimalistic Linux distribution is another way to reduce the footprint of the deployment.

目前很多举措正在改善 Java 对容器的支持。例如,OpenJDK 9 提供了一个 Alpine Linux 端口。在这个简约的 Linux 发行版之上运行 JDK 或自定义运行时映像是减少部署空间的另一种方法。

Another advantage of distributing the Java runtime with your application: there’s no more mismatch between already installed Java versions and the version your application needs. Traditionally, either a Java Runtime Environment (JRE) or Java Development Kit (JDK) must be installed before running Java applications.

将应用程序与 Java 运行时一起分发的另一个优点是:已经安装的 Java 版本与应用程序需要版本之间不再有更多的不匹配。传统上,在运行 Java 应用程序之前必须安装 Java RuntimeEnvironment(JRE)或 Java Development Kit(JDK)。

NOTE

The JRE is a subset of the JDK, designed to run Java applications rather than to develop Java applications. It has always been offered as a separate download geared toward end users.

JRE 是 JDK 的一个子集,用于运行 Java 应用程序,而不是开发 Java 应用程序。它始终作为一个单独的下载提供给最终用户。

A custom runtime image is fully self-contained. It bundles the application modules with the JVM and everything else it needs to execute your application. No other Java installation (JDK/JRE) is necessary. Distributing, for example, JavaFX-based desktop applications becomes easier this way. By creating a custom runtime image, you can offer a single download containing the application and the runtime environment. On the flip side, an image is not portable, because it targets a specific OS and architecture. In “Cross-Targeting Runtime Images”, we’ll discuss how to create images for different target systems.

自定义运行时映像是完全独立的。它将应用程序模块与 JVM 以及执行应用程序所需的一切捆绑在一起,而不需要其他的 Java 安装(JDK/JRE)。例如,分发基于 JavaFX 的桌面应用程序会变得更容易。通过创建自定义运行时映像,可以提供包含应用程序以及运行时环境的单个下载。另一方面,映像不可移植,因为它的目标是特定的操作系统和体系结构。在 13.8 节中,将讨论如何为不同的目标系统创建映像。

It’s time to take a deep dive into what jlink is capable of. Before you look at the tool itself, we’ll first discuss linking and how it opens up new possibilities for Java applications.

接下来深入了解一下 jlink 的能力。在介绍该工具本身之前,首先讨论一下链接以及它如何为 Java 应用程序开辟新的可能性。

13.1 Static Versus Dynamic Linking 静态链接和动态链接

Creating a custom runtime image can also be characterized as a form of static linking for modules. Linking is the process of bringing together compiled artifacts into an efficiently executable form. Traditionally, Java has always employed dynamic linking at the class level. Classes are lazily loaded at run-time, and wired together dynamically where necessary at any point in time. The just-in-time (JIT) compiler of the virtual machine is then responsible for compilation to native code at run-time. During this process, the JVM applies optimizations to the resulting ensemble of classes. Although this model allows for great flexibility, optimizations that are straightforward in more static situations are harder (or even impossible) to apply in this dynamic landscape.

可以将创建自定义运行时映像描述为模块的一种静态链接形式。链接(linking)是将已编译工件组合成高效可执行形式的过程。传统上,Java 一直在类级别上采用动态链接(dynamiclinking)。类在运行时被延迟加载,并且必要时在任何时间点进行动态链接。然后,虚拟机的即时(just-in-time, JIT)编译器负责在运行时将其编译为本机代码。在这个过程中,JVM 对生成的类集进行优化。虽然这个模型可以提供很大的灵活性,但是在更静态的情况下,简单的优化比在动态的场景中更难(甚至不可能)应用。

Other languages make different trade-offs. Go, for example, prefers to statically link all code into a single binary. In C++, you can choose to link statically or dynamically. With the introduction of the module system and jlink, we now have that choice in Java as well. Classes are still dynamically loaded and linked in custom runtime images. However, the available modules from which classes can be loaded can be statically predetermined.

不同语言采取了不同的折中方案。比如,Go 语言偏向于将所有代码静态链接到一个二进制文件中。在 C++中,可以选择静态或动态链接。随着模块系统和 jlink 的引入,现在在 Java 中也有了这个选择。类仍然动态加载并链接到自定义运行时映像中。但是,可以静态预先确定从可用模块中加载哪些类。

An advantage of static linking is that it affords optimizations across the whole application ahead of time. Effectively, this means optimizations can be applied across class and module boundaries, taking into account the whole application. This is possible only because through the module system we have up-front knowledge about what the whole application actually entails. All modules are known, from the root module (the entry point of an application), to the libraries, all the way through to the required platform modules. A resolved module graph represents the whole application.

静态链接的一个优点是可以提前对整个应用程序进行优化。实际上,这意味着从整个应用程序为出发点进行考虑,可以跨类和模块边界应用优化。这是可能的,因为通过模块系统,对整个应用程序的实际需求提前有了了解。所有模块都是已知的,从根模块(应用程序的入口点)到库,然后到所需的平台模块。已解析模块图显示了整个应用程序。

WARNING

Linking is not the same as ahead-of-time (AOT) compilation. A runtime image generated with jlink still consists of bytecode, not native code.

链接与提前(AOT)编译不同。用 jlink 生成的运行时映像仍然由字节码组成,而不是本地代码。

Examples of whole-program optimizations are dead-code elimination, constant folding, and inlining. It’s beyond the scope of this book to describe these optimizations. Fortunately, much literature is available.1

整个程序优化的示例包括死代码消除(dead-code elimination)、常量合并(constantfolding)和内联。虽然介绍这些优化已经超出了本书的范围,但幸运的是很多文献都提供了相关内容。

第 13 章 使用自定义运行时映像进行缩减 - 图1

The applicability and effectiveness of many of these optimizations hinges on the assumption that all relevant code is available at the same time. The linking phase is that time, and jlink is the tool to do so.

许多优化的适用性和有效性取决于相关代码同时可用的假设。链接阶段就是这个假设成立的时候,而 jlink 就是完成优化的工具。

13.2 Using jlink 使用 jlink

Back in “Linking Modules”, we created a custom runtime image consisting of just a helloworld module and java.base. Toward the end of Chapter 3, we created a much more interesting application: EasyText, with its multiple analyses and CLI/GUI frontends. Just as a reminder, running the GUI frontend on top of a full JDK is achieved by setting up the right module path and starting the right module:

在 3.1.7 节中,创建了一个由 helloworld 模块和 java.base 组成的自定义运行时映像。在第 3 章的末尾,创建了一个更有趣的应用程序——EasyText,实现了多重分析和 CLI/GUI 前端。提醒一下,在完整的 JDK 上运行 GUI 前端是通过设置正确的模块路径并启动正确的模块来实现的:

  1. $ java -p mods -m easytext.gui

Assuming the mods directory contains the modular JARs for EasyText, this results in the situation depicted in Figure 13-1 at run-time.

假设 mods 目录包含用于 EasyText 的模块化 JAR,从而在运行时出现如图 13-1 所示的情况。

Modules resolved at run-time when running easytext.gui. Grayed-out modules are available in the JDK but not resolved.

At JVM startup, the module graph is constructed. Starting with the root module easytext.gui, all dependencies are recursively resolved. Both application modules and platform modules are part of the resolved module graph. However, as Figure 13-1 shows, many more platform modules are available in the JDK than strictly necessary for this application. The grayed-out modules are only the tip of the iceberg, because about 90 platform modules exist. Only nine of them are necessary to run easytext.gui with its JavaFX UI.

在 JVM 启动时,创建了该模块图。从根模块 easytext.gui 开始,所有依赖项都被递归解析。应用程序模块和平台模块都是解析模块图的一部分。但是,如图 13-1 所示,JDK 中所提供的平台模块并不是都是应用程序所必需的。灰色的模块只是冰山一角,因为大约有 90 个平台模块存在。其中只有 9 个平台模块是使用 JavaFX UI 运行 easytext.gui 所必要的。

Let’s get rid of this dead weight by creating a custom runtime image for EasyText. The first choice we must make is, what exactly should be part of the image? Just the GUI, just the CLI, or both? You could create multiple images targeting distinct groups of users of your application. There’s no general right or wrong answer to this question. Linking is explicitly composing modules together into a coherent whole.

接下来通过为 EasyText 创建一个自定义运行时映像来摆脱上述沉重负担。所要做的第一选择是确定哪些模块应该成为映像的一部分?是 GUI,或是 CLI,还是两者兼而有之?可以针对应用程序的不同用户组创建多个映像。针对这个问题没有常见的正确或错误的答案。链接显式地将模块组合成一个连贯的整体。

For now, let’s settle on creating a runtime image just for the GUI version of EasyText. We invoke jlink with easytext.gui as the root module:

现在,为 EasyText 的 GUI 版本创建一个运行时映像。此时使用 easytext.gui 作为根模块调用 jlink:

  1. $ jlink --module-path mods/:$JAVA_HOME/jmods \ 1
  2. --add-modules easytext.gui \ 2
  3. --add-modules easytext.algorithm.coleman \
  4. --add-modules easytext.algorithm.kincaid \
  5. --add-modules easytext.algorithm.naivesyllablecounter \
  6. --launcher easytext=easytext.gui \ 3
  7. --output image 4
  1. Set the module path where jlink can find modules, including platform modules in the JDK.
  2. Add root modules to be included in the runtime image. Besides easytext.gui, service provider modules are added as root modules as well.
  3. Define the name of a launcher script to be part of the runtime image, indicating the module it should run.
  4. Set the output directory where the image is generated into.

  1. 设置 jlink 可以找到模块的模块路径,包括 JDK 中的平台模块。
  2. 添加要包含在运行时映像中的根模块。除了 easytext.gui 外,服务提供者模块也作为根模块被添加。
  3. 将启动器脚本的名称定义为运行时映像的一部分,指示它应该运行的模块。
  4. 设置映像生成的输出目录。

TIP

The jlink tool lives in the bin directory of the JDK installation. It is not added to the system path by default, so in order to use it as shown in the preceding example, you must add it to the path first.

jlink 工具位于 JDK 安装的 bin 目录中。它不会默认添加到系统路径中,所以如果想要像本示例这样使用它,必须要先将其添加到路径中。

It’s possible to provide multiple root modules separated by commas as well, instead of the multiple —add-modules used here.

可以提供多个以逗号分隔的根模块,而不是像此处使用多个—add-modules。

The specified module path explicitly includes the JDK directory containing platform modules (\$JAVA_HOME/jmods). This is different from what you’ve seen when using java and javac: there the platform modules are assumed to come from the same JDK you run java or javac from. In “Cross-Targeting Runtime Images”, you’ll see why this is different for jlink.

指定的模块路径显式包含了包含平台模块(\$ JAVA_HOME/jmods)的 JDK 目录。这与使用 java 和 javac 时所看到的不同:假设平台模块来自运行 java 或 javac 的同一个 JDK。在 13.8 节中将会解释为什么 jlink 会有所不同。

As discussed in “Services and Linking”, service provider modules must be added as root modules as well. Resolving the module graph happens only through requires clauses; jlink does not follow uses and provides dependencies by default. In “Finding the Right Service Provider Modules”, we’ll show you how to find the right service provider modules to add.

正如在 4.7 节中所讨论的那样,服务提供者模块也必须作为根模块添加。只有通过 requires 子句才会对模块图进行解析;默认情况下 jlink 并不遵循 uses 和 provides 依赖关系。在 13.3 节中,将演示如何找到要添加的正确的服务提供者模块。

NOTE

You can add the —bind-services flag to jlink. This instructs jlink to take into account uses/provides as well when resolving modules. However, this also binds all services between platform modules. Because java.base already uses a lot of (optional) services, this quickly leads to a larger set of resolved modules than strictly necessary.

可以将—bind-services 标志添加到 jlink,从而指示 jlink 在解析模块时考虑 uses/provides。但是,这样一来就绑定了平台模块之间的所有服务。因为 java.base 已经使用了很多(可选的)服务,所以这样做会导致重复解析更多不需要的已解析模块。

Each of these root modules is resolved, and these root modules along with their recursively resolved dependencies become part of the generated image in ./image, as shown in Figure 13-2.

这些根模块中的每一个都已经被解析,这些根模块及其递归解析的依赖项成为./image 中生成映像的一部分,如图 13-2 所示。

A custom runtime image contains only the modules necessary for the application

The generated image has the following directory layout, which is similar to the JDK:

生成的映像包含了以下所示类似于 JDK 的目录层次:

  1. image
  2. ├── bin
  3. ├── conf
  4. ├── include
  5. ├── legal
  6. └── lib

In the bin directory of the generated runtime image, you’ll find an easytext launcher script. It’s created because of the —launcher easytext=easytext.gui flag. The first argument is the name of the launcher script, and the second argument is the module it starts. This script is an executable convenience wrapper that starts the JVM directly with easytext.gui as the initial module to run. You can directly execute it from the command line by invoking image\bin\easytext. On other platforms, similar scripts are generated (see “Cross-Targeting Runtime Images” for how to target other platforms). Windows runtime images get batch files instead of shell scripts for Unix-like targets.

在生成的运行时映像的 bin 目录中可以找到一个 easytext 启动器脚本,它是因为—launchereasytext = easytext.gui 标志而创建的。其中第一个参数是启动器脚本的名字,第二个参数是启动的模块。该脚本是一个可执行的便利包装器,它以 easytext.gui 作为运行的初始模块直接启动 JVM。可以从命令行通过调用 image\bin\easytext 直接执行脚本。在其他平台上,也会生成类似的脚本(请参阅 13.8 节以了解如何针对其他平台生成类似脚本)。Windows 运行时映像获取的是批处理文件,而不是针对类似 UNIX 目标的 shell 脚本。

A launcher script can be created for modules containing a class with an entry point (static main method). That’s the case if we create the easytext.gui modular JAR as follows:

可以为那些包含一个入口点(静态主方法)的类的模块创建启动器脚本。如果按如下方式创建 easytext.gui 模块化 JAR,就属于这种情况:

  1. jar --create \
  2. --file mods/easytext.gui.jar \
  3. --main-class=javamodularity.easytext.gui.Main \
  4. -C out/easytext.gui .

You can also build a runtime image from exploded modules. In that case, there’s no main class attribute from the modular JAR, so it must be explicitly added to the jlink invocation:

还可以通过分解模块构建运行时映像。此时,模块化 JAR 中没有主类属性,所以必须显式地将其添加到 jlink 调用中:

  1. --launcher easytext=easytext.gui/javamodularity.easytext.gui.Main

This approach also works if there are multiple classes with main methods in a modular JAR. Regardless of whether specific launcher scripts are created, you can always start the application by using the image’s java command:

即使在模块化 JAR 中有多个带有主方法的类,此方法也适用。无论是否创建了特定的启动器脚本,都可以使用映像的 java 命令来启动应用程序:

  1. image/bin/java -m easytext.gui/javamodularity.easytext.gui.Main

There’s no need to set the module path when running the java command from the runtime image. All necessary modules are already in the image through the linking process.

当从运行时映像运行 java 命令时,不需要设置模块路径,所有必要的模块已经通过链接过程包含在映像中。

We can show that the runtime image indeed contains the bare minimum of modules by running the following:

可以通过运行以下命令来验证运行时映像确实包含了最少的模块:

  1. $ image/bin/java --list-modules
  2. easytext.algorithm.api@1.0
  3. easytext.algorithm.coleman@1.0
  4. easytext.algorithm.kincaid@1.0
  5. easytext.algorithm.naivesyllablecounter@1.0
  6. easytext.gui@1.0
  7. java.base@9
  8. java.datatransfer@9
  9. java.desktop@9
  10. java.prefs@9
  11. java.xml@9
  12. javafx.base@9
  13. javafx.controls@9
  14. javafx.graphics@9
  15. jdk.jsobject@9

This corresponds to the modules shown in Figure 13-2.

上述内容与图 13-2 中所示的模块相对应。

The bin directory can contain other executables besides the launcher scripts discussed so far. In the EasyText GUI image, the keytool and appletviewer binaries are also added. The former is always present, because it originates from java.base. The latter is part of the image because the included java.desktop module exposes applet functionality. Because other well-known JDK command-line tools such as jar, rmic, and javaws all depend on modules that are not in this runtime image, jlink is smart enough to omit them.

除了包含了到目前为止所讨论的启动器脚本以外,bin 目录还可以包含其他可执行文件。在 EasyText GUI 映像中,还添加了 keytool 和 appletviewer 二进制文件。前者始终存在,因为它来自 java.base。后者是映像的一部分,因为所包含的 java.desktop 模块公开了小程序功能。由于其他 JDK 命令行工具(比如 jar、rmic 和 javaws)所依赖的模块并没有包含在此运行时映像中,因此 jlink 足够聪明以忽略这些模块。

13.3 Finding the Right Service Provider Modules 查找正确的服务提供者模块

In the previous EasyText jlink example, we added several service provider modules as root modules. As mentioned earlier, it is possible to use the —bind-services option of jlink and let jlink resolve all service provider modules from the module path instead. While tempting, this quickly leads to an explosion of modules in the resulting image. Blindly adding all possible service providers for a given service type is rarely the right thing to do. It pays to think about what service providers are right for your application and add those as root modules yourself.

在前面的 EasyText jlink 示例中,添加了几个服务提供者模块作为根模块。如前所述,可以使用 jlink 的—bind-services 选项,并让 jlink 从模块路径中解析所有的服务提供者模块。虽然这种做法极具诱惑力,但也会导致映像中模块数量激增。盲目地为给定的服务类型添加所有可能的服务提供者是不正确的。需要认真考虑一下哪些服务提供者适合应用程序,并将其作为根模块添加。

Fortunately, you can get help from jlink in selecting the right service provider modules by using the —suggest-providers option. In the following jlink invocation, we add only the easytext.gui module and ask for suggestions of provider modules for the Analyzer type:

幸运的是,可以通过使用—suggest-providers 选项来帮助选择合适的服务提供者模块。在下面所示的 jlink 调用中,只添加了 easytext.gui 模块,并询问 Analyzer 类型的提供者模块的建议:

  1. $ jlink --module-path mods/:$JAVA_HOME/jmods \
  2. --add-modules easytext.gui \
  3. --suggest-providers javamodularity.easytext.algorithm.api.Analyzer
  4. Suggested providers:
  5. module easytext.algorithm.coleman provides
  6. javamodularity.easytext.algorithm.api.Analyzer,
  7. used by easytext.cli,easytext.gui
  8. module easytext.algorithm.kincaid provides
  9. javamodularity.easytext.algorithm.api.Analyzer,
  10. used by easytext.cli,easytext.gui

You can then select one or more provider modules by adding them with --add-modules <module>. Of course, when these newly added modules have uses clauses themselves, another invocation of —suggest-providers is in order. For example, in EasyText, the easytext.algorithm.kincaid service provider module itself has a uses constraint on the SyllableCounter service type.

然后,可以通过添加--add-modules <module>来选择一个或多个提供者模块。当然,当这些新添加的模块本身包含了 uses 子句时,则会按顺序再次调用—suggest-providers。例如,在 EasyText 中,easytext.algorithm.kincaid 服务提供者模块本身对 SyllableCounter 服务类型使用了 uses 约束。

It’s also possible to leave off the specific service type after —suggest-providers and get a complete overview. This includes service providers from platform modules as well, so the output can quickly get overwhelming.

也可以在—suggest-providers 之后放弃特定的服务类型并获得完整的概述,其中可能包括来自平台模块的服务提供者,所以输出可能会很快变得难以控制。

13.4 Module Resolution During Linking 链接期间的模块解析

Although the module path and module resolution in jlink appear to behave similarly to other tools you’ve seen so far, important differences exist. One exception you’ve already seen is that platform modules need to be added explicitly to the module path.

尽管 jlink 中的模块路径和模块解析在行为上与前面所介绍的其他工具类似,但也存在一些重要差异。其中一个例外是平台模块需要显式地添加到模块路径。

Another important exception involves automatic modules. When you put a nonmodular JAR on the module path with java or javac, it is treated as valid module for all intents and purposes (see “Automatic Modules”). However, jlink does not recognize nonmodular JARs on the module path as automatic modules. Only when your application is fully modularized, including all libraries, can you use jlink.

另一个重要的例外涉及自动模块。当使用 java 或 javac 在模块路径中放置非模块化 JAR 时,其将被视为满足所有意图和目的的有效模块(请参见 8.4 节)。但是,jlink 不会将模块路径上的非模块 JAR 识别为自动模块。只有当应用程序完全模块化时(包括所有库),才可以使用 jlink。原因是自动模块可以从类路径中读取,从而绕过模块系统的显式依赖关系。在自定义运行时映像中没有预定义的类路径,因此所生成映像中的自动模块可能会导致运行时异常。当它们依赖于类路径中不存在的类时,应用程序会在运行时崩溃。一旦出现这种情况,就会导致模块系统可靠配置保证的失效。

The reason is automatic modules can read from the classpath, bypassing the module system’s explicit dependencies. In a custom runtime image, there is no predefined classpath. Automatic modules in the resulting image could therefore cause run-time exceptions. When they rely on classes from the classpath that won’t be there, the application blows up at run-time. Allowing this situation voids the reliable configuration guarantees of the module system.

当然,如果确定一个自动模块的行为良好(也就是说,它仅需要其他模块,并且不需要从类路径中获取模块),那么就应该避免上述情况的出现。此时,可以通过将自动模块转换为显式模块来消除此限制。可以使用 jdeps 来生成模块描述符(如 10.3 节中所述),并将其添加到 JAR 中。现在有了一个可以放在 jlink 模块路径上的模块。但这并不是一个理想的情况;仅修补别人的代码永远不会得到理想的代码。自动模块只是一个用来帮助迁移的过渡功能。当遇到这种 jlink 限制时,也就是联系问题库的维护者并督促他们实现模块化的好时机。

Of course, when you’re absolutely sure an automatic module is well behaved (i.e., it requires only other modules and nothing from the classpath), this situation is unfortunate. In that case, you can work around this limitation by turning the automatic module into an explicit module. You can use jdeps to generate the module descriptor (as discussed in “Creating a Module Descriptor”) and add it to the JAR. Now you have a module that can be put on jlink’s module path. This is not an ideal situation; patching other people’s code never is. Automatic modules are really a transitional feature to aid migration. When you run into this jlink limitation, it’s a good time to contact the maintainer of the library in question to urge them to modularize it.

当然,如果确定一个自动模块的行为良好(也就是说,它仅需要其他模块,并且不需要从类路径中获取模块),那么就应该避免上述情况的出现。此时,可以通过将自动模块转换为显式模块来消除此限制。可以使用 jdeps 来生成模块描述符(如 10.3 节中所述),并将其添加到 JAR 中。现在有了一个可以放在 jlink 模块路径上的模块。但这并不是一个理想的情况;仅修补别人的代码永远不会得到理想的代码。自动模块只是一个用来帮助迁移的过渡功能。当遇到这种 jlink 限制时,也就是联系问题库的维护者并督促他们实现模块化的好时机。

Last, the same caveats with respect to modules using reflection apply during linking, as with running modules on a full JDK. If a module uses reflection and does not list those dependencies in the module descriptor, the resolver cannot take this into account. Consequently, the module containing the code that is reflected upon may not be included in the image. To prevent this, add the modules manually with —add-modules, so they end up in the runtime image anyway.

最后,在链接期间,使用反射模块时的注意事项仍然适用,就像在完整的 JDK 上运行模块一样。如果一个模块使用了反射并且没有在模块描述符中列出依赖项,那么解析器对此将不予考虑。因此,包含被反射代码的模块可能并不包含在映像中。为了防止出现这种情况,请使用—add-modules 手动添加模块,以便它们最终出现在运行时映像中。

13.5 jlink for Classpath-Based Applications 基于类路径应用程序的 jlink

It seems as though jlink is usable only with fully modularized applications. That’s true only in part. You can also use jlink to create custom images of the Java platform without involving any application modules. For example, you can run

似乎 jlink 只能用于完全模块化的应用程序,这只能说是部分正确。还可以使用 jlink 创建 Java 平台的自定义映像,而不包括任何应用程序模块。例如,可以运行

  1. jlink --module-path $JAVA_HOME/jmods --add-modules java.logging --output image

and get an image containing just the java.logging (and the mandatory java.base) module. That may seem only marginally useful, but this is where things get interesting. If you have an existing classpath-based application, there’s no way to use jlink directly on application code to create a custom runtime image for that application. There are no module descriptors for jlink to resolve any modules.

并获得一个只包含 java.logging(同时强制包含 java.base)模块的映像。虽然这么做用处不大,但却使事情变得更有趣。如果有一个现成的基于类路径的应用程序,则无法直接在应用程序代码上使用 jlink 为该应用程序创建自定义运行时映像。jlink 没有模块描述符来解析任何模块。

However, what you can do is use a tool such as jdeps to find the minimal set of platform modules necessary to run the application. If you then construct an image containing just these modules, you can later start your classpath-based application on this image without issues.

但是,可以使用诸如 jdeps 之类的工具来查找运行该应用程序所需的最小平台模块集。如果仅构建一个包含这些模块的映像,那么可以在此映像上启动基于类路径的应用程序,而不会出现问题。

That probably sounds a bit abstract, so let’s look at a simple example. In “Using the Modular JDK Without Modules”, Example 2-5, we already had a simple NotInModule class that uses the java.logging platform module. We can inspect this class with jdeps to verify that is the only dependency:

这可能听起来有点抽象,接下来看一个简单的示例。在示例 2-5 中已经创建了一个简单的 NotInModule 类,它使用了 java.logging 平台模块。可以用 jdeps 检查该类,从而验证它是唯一的依赖项:

  1. $ jdeps out/NotInModule.class
  2. NotInModule.class -> java.base
  3. NotInModule.class -> java.logging
  4. <unnamed> -> java.lang java.base
  5. <unnamed> -> java.util.logging java.logging

For larger applications, you would analyze the JARs of the application and its libraries in the same manner. Now you know which platform modules are necessary to run the application. For our example, that’s just java.logging. We already created the image containing only the java.logging module earlier in this section. By putting the NotInModule class on the classpath of the runtime image, we can run this classpath-based application on a stripped-down Java distribution:

对于较大的应用程序,可以使用相同的方式分析应用程序的 JAR 及其库。现在可以知道运行应用程序所需的平台模块了。对于上面的示例,只需要 java.logging 模块。同时,还创建了仅包含 java.logging 模块的映像。只需将 NotInModule 类放在运行时映像的类路径上,就可以在一个简化的 Java 发行版上运行这个基于类路径的应用程序:

  1. image/bin/java -cp out NotInModule

The NotInModule class is on the classpath (assuming it’s in the out directory), so it is in the unnamed module. The unnamed module reads all other modules, which, in the case of this custom runtime image, is the tiny set of two modules: java.base and java.logging. Through these steps, you can create custom runtime images even for classpath-based applications. Had EasyText been a classpath-based application, the same steps would result in an image containing the nine platform modules shown in Figure 13-1.

由于 NotInModule 类在类路径上(假设位于 out 目录中),因此它位于未命名的模块中。未命名模块读取其他模块,在自定义运行时映像的情况下,该未命名模块是一个包含两个模块(java.base 和 java.logging)的小集合。通过这些步骤,甚至可以为基于类路径的应用程序创建自定义运行时映像。如果 EasyText 是基于类路径的应用程序,则使用相同的步骤会产生包含如图 13-1 所示的九平台模块的映像。

There are some caveats. When your application tries to load classes from modules that are not in the runtime image, NoClassDefFoundError is raised at run-time. This is similar to the situation where your classpath lacks a necessary JAR. It is up to you to list the right set of modules for jlink to put into the image. At link-time, jlink doesn’t even see your application code, so it cannot help with module resolution as it can in the case of a fully modularized application. jdeps is helpful to get a first estimation of the required modules, but use of reflection in your application can, for example, not be detected by the static analysis of jlink. Testing your application on the resulting image is therefore important.

有一些需要关注的注意事项。当应用程序试图从不在运行时映像的模块中加载类时,会在运行时触发 NoClassDefFoundError。这类似于在类路径中缺少必要 JAR 的情况。哪些模块应该放到映像中完全由你来决定。在链接时,jlink 甚至不会查看应用程序代码,所以它无法像在一个完全模块化应用程序中那样帮助模块解析。虽然 jdeps 有助于估计所需的模块,但 jlink 的静态分析无法检测出应用程序中反射的使用。因此,在所产生的映像上测试应用程序是非常重要的。

Furthermore, not all performance optimizations discussed in the subsequent sections are applicable to this classpath-based scenario. Many optimizations work because all code is available at link-time—which is not the case in the scenario we just discussed. The application code is never seen by the linker. In this scenario, jlink works only with the platform modules you add to the image explicitly, and their resolved platform module dependencies.

此外,后续章节所讨论的性能优化并不都适用于基于类路径的场景。许多优化之所以有效,是因为所有的代码在链接时都可用——而前面所讨论的并不是这种情况。链接器并不会查看应用程序代码。在这种情况下,jlink 只能使用显式添加到映像中的平台模块以及已解析的平台模块依赖项。

This use of jlink shows how the lines between the JDK and JRE have blurred in a modular world. Linking allows you to create a Java distribution with any desired set of platform modules. You’re not confined to the options offered by the platform vendors.

jlink 的这种用法显示了 JDK 和 JRE 之间的界限在模块化的世界中如何变得模糊。链接允许使用任何所需的一组平台模块来创建 Java 分发,并且不局限于平台供应商提供的选项。

13.6 Reducing Size 压缩大小

Now that we have the basic use of jlink covered, it’s time to turn our attention to the optimizations we were promised. jlink uses a plug-in-based approach to support different optimizations. All flags introduced in this section and the next section are handled by jlink plug-ins. The aim is not to give an exhaustive overview of all plug-ins, but rather to highlight some plug-ins to illustrate the possibilities. The number of jlink plug-ins is expected to grow steadily over time, both by the JDK team and the community at large.

前面已经介绍了 jlink 的基本用法,接下来可以将注意力转移到优化方面的问题了。jlink 使用基于插件的方法来支持不同的优化。本节以及下一节介绍的所有标志都由 jlink 插件处理,其目的不是要对所有插件进行详尽的概述,而是突出一些插件来说明可能性。随着时间的推移,jlink 插件的数量预计会稳步增长,这些插件可能来自 JDK 团队或者整个社区。

You can get an overview of all currently available plug-ins by running jlink —list-plugins. Some plug-ins are enabled by default; this is indicated in the output of this command. In this section, we’ll look at plug-ins that can reduce the disk size of runtime images. The next section covers run-time performance improvements.

可以通过运行 jlink —list-plugins 来获得所有当前可用插件的概述。一些插件默认是启用的,它们将在上述命令的输出中显示。本节将着重介绍可以减小运行时映像的磁盘大小的插件。下一节将介绍运行时性能的改进。

Creating a custom runtime image as we did for EasyText already reduces the size on disk relative to a full JDK by leaving out unnecessary platform modules. Still, there’s more to be gained. You can use several flags to trim the image size even further.

就像为 EasyText 所做的那样创建一个自定义运行时映像可以省去不必要的平台模块,从而减小所需磁盘的容量(相对于使用完整的 JDK 而言)。除此之外,还有更多的收获。可以使用多个标志进一步减小映像的大小。

The first one is —strip-debug. As the name implies, it removes native debug symbols and strips debug information from classes. For a production build, this is what you want. However, it is not enabled by default. Empirical evidence shows roughly a 10 percent reduction in image size with this flag enabled.

第一个标志是—strip-debug。顾名思义,它将删除本地调试符号,并从类中去除调试信息。对于生产版本来说,上述功能都是需要的。但是在默认情况下该标志不启用。经验表明,启用此标志后,映像大小大约减小了 10%。

You can also compress the resulting image with the —compress=n flag. Currently, n can be 0, 1, or 2. It does two things on the highest setting. First, it creates a table of all string literals used in classes, sharing the representation across the whole application. Then, it applies a generic compression algorithm on the modules.

也可以使用—compress = n 标志压缩生成的映像。目前,n 可以是 0、1 或 2。如果设置为最高值,则完成了两件事情。首先,创建一个在类中使用的所有字符串文本表,从而在整个应用程序中共享表示形式。然后,在模块上应用通用的压缩算法。

The next optimization affects the JVM that is put into the runtime image. With --vm=<vmtype>, different types of VMs can be selected. Valid options for vmtype are server, client, minimal, and all (which is the default). If a small footprint is your most important concern, choose the minimal option. It offers a VM with only a single garbage collector, one JIT compiler, and no serviceability or instrumentation support. At the moment, the minimal VM option is available only for Linux.

下一个优化会影响放入到运行时映像的 JVM。通过使用--vm = <vmtype>,可以选择不同类型的 VM。vmtype 的有效选项包括 server、client、minimal 和 all(此为默认值)。如果减少占用空间是最关心的问题,那么请选择 minimal 选项。它所提供的 VM 仅包含一个垃圾收集器以及一个 JIT 编译器,没有可维护性或检测支持。目前,最小的 VM 选项仅适用于 Linux。

The last optimization concerns locales. Normally, the JDK ships with many locales to suit different date/time formats, currencies, and other locale-sensitive information. The default English locales are part of java.base, and so are always available. All other locales are part of the module jdk.localedata. Because this is a separate module, you can choose to add it to a runtime image or not. Services are used to expose the locale functionality from java.base and jdk.localedata. This means you must use —add-module jdk.localedata during linking if your application needs a non-English locale. Otherwise, locale-sensitive code simply falls back to the default English locale, because no other locales are available in the absence of jdk.localedata. Remember, service provider modules are not resolved automatically, unless —bind-services is used.

最后一个优化涉及语言环境。通常,JDK 附带许多语言环境,以适应不同的日期/时间格式、货币和其他区域敏感信息。英文的默认语言环境是 java.base 的一部分,因此始终可用。所有其他语言环境都是模块 jdk.localedata 的一部分。由于这是一个单独的模块,因此可以选择是否将其添加到运行时映像。可以使用服务以公开来自 java.base 和 jdk.localedata 的语言环境功能。这意味着如果应用程序需要非英文语言环境,则必须在链接期间使用—add-modulejdk.localedata。否则,语言环境敏感代码会回到默认的英语语言环境,因为在没有 jdk.locale.data 的情况下没有其他语言环境可用。请记住,除非使用—bind-services,否则不会自动解析服务提供者模块。

TIP

A similar situation arises when your application uses character sets that are not part of the default sets (i.e., US-ASCII, ISO-8859-1, UTF-8, UTF-16). Traditionally, these nondefault character sets were available through charsets.jar in the full JDK. With the modular JDK, add the jdk.charsets module to your image in order to use nondefault character sets.

当应用程序使用不属于默认集(即 US-ASCII、ISO-8859-1、UTF-8、UTF-16)的字符集时,也会出现类似情况。传统上,这些非默认字符集可以通过完整 JDK 中的 charsets.jar 获得。如果使用模块化 JDK,为了使用非默认字符集,需要将 jdk.charsets 模块添加到映像中。

However, jdk.locale is quite large. Adding this module easily increases the size on disk with about 15 megabytes. In many cases, you don’t need all locales in the image. If that’s the case, you can use the —include-locales flag of jlink. It takes a list of language tags as an argument (see the java.util.Locale JavaDoc for more information on valid language tags). The jlink plug-in then strips the jdk.locale module from all other locales. Only the resources for the specified locales will remain in the jdk.locale module that ends up in the image.

然而,jdk.locale 相当大。添加该模块会增加大约 15MB 的磁盘大小。在许多情况下,不需要在映像中包含所有的语言环境信息。如果是这样的话,可以使用 jlink 的—include-locales 标志。它将语言标记列表作为参数(有关有效语言标记的更多信息请参阅 java.util.Locale JavaDoc)。然后 jlink 插件从所有其他语言环境中删除 jdk.locale 模块。最终,只有指定语言环境的资源才会保留在映像中的 jdk. locale 模块中。

13.7 Improving Performance 提升性能

The previous section presented a range of optimizations pertaining to the size of the image on disk. What’s even more interesting is the ability of jlink to optimize run-time performance. Again, this is achieved by plug-ins, some of which are enabled by default. Keep in mind that jlink and its plug-ins are still at an early stage and mostly aimed at improving startup time of applications. Many of the plug-ins discussed in this section are still experimental in JDK 9. Most of the performance optimizations available require in-depth knowledge of how the JDK works.

上一节介绍了一系列有关磁盘上映像大小的优化,而更令人感兴趣的是 jlink 优化运行时性能的能力。这一切都是通过插件实现的,其中一些插件是默认启用的。请记住,jlink 及其插件目前还处于初级阶段,主要是为了改善应用程序的启动时间。本节所讨论的许多插件在 JDK 9 中仍然是实验性的。大多数可用的性能优化需要深入了解 JDK 的工作原理。

A lot of the experimental plug-ins generate code at link-time to improve startup performance. One optimization that is enabled by default is the pre-creation of a platform module descriptor cache. The idea is that when building the image, you know exactly which platform modules are part of the module graph. By creating a combined representation of their module descriptors at link-time, the original module descriptors don’t have to be parsed individually at run-time. This decreases the JVM startup time.

很多实验性插件在链接时生成代码来提高启动性能。默认启用的一种优化是预先创建平台模块描述符缓存,其主要想法是在构建映像时确切知道哪些平台模块是模块图的一部分。如果在链接时创建了模块描述符的组合表示,那么就没有必要在运行时单独解析原始模块描述符了,从而减少 JVM 启动时间。

In addition, other plug-ins perform bytecode rewriting at link-time to achieve run-time performance gains. An example is the —class-for-name optimization. It rewrites instructions of the form Class.forName(“pkg.SomeClass”) to static references to that class, thus avoiding the overhead of reflectively searching for the class at run-time. Another example is a plug-in that pre-generates method handles invocation classes (extending java.lang.invoke.MethodHandle), which would otherwise be generated at run-time. While this may sound esoteric, the implementation of Java lambdas makes heavy use of the method handles machinery. By absorbing the cost of class generation at link-time, applications that use lambdas start faster. Unfortunately, using this plug-in currently requires highly specialized knowledge of the way method handles work.

另外,其他插件在链接时执行了字节码重写,从而提升了运行时性能。比如—class-for-name 优化就是一个示例。它将形式为 Class.forName(“pkg.SomeClass”)的指令重写为对该类的静态引用,从而避免了在运行时反射式搜索类所带来的开销。另一个示例是预生成方法处理调用类的插件(扩展 java.lang.invoke. MethodHandle),否则这些类将会在运行时生成。虽然这可能听起来很深奥,但 Java Lambda 表达式的实现使得可以大量使用这种方法处理机制。通过减少链接时类生成的成本,大大加快了使用 Lambda 表达式的应用程序的启动速度。但不幸的是,目前使用该插件需要对方法处理工作的方式有比较深入的了解。

As you can see, many plug-ins offer quite specialized performance-tuning scenarios. There are so many possible optimizations, a tool such as jlink is never done. Some optimizations are better suited to some applications than others. This is one of the prime reasons jlink features a plug-in-based architecture. You can even write your own jlink plug-ins, although the plug-in API itself is marked experimental in the Java 9 release.

正如所看到的,许多插件提供了相当专业的性能调整方案,有很多可能的优化是诸如 jlink 这样的工具所无法完成的。有些优化比其他优化更适合于某些应用程序。这是 jlink 提供基于插件的体系结构的主要原因之一。甚至可以编写自己的 jlink 插件,尽管插件 API 本身在 Java 9 版本中被标记为实验性的。

Optimizations that have traditionally been too costly to perform just-in-time by the JVM are now viable at link-time. What’s remarkable is that jlink can optimize code coming from any module, whether it’s an application module, a library module you use, or a platform module. However, because jlink plug-ins allow arbitrary bytecode rewriting, their usefulness extends beyond performance enhancements. Many tools and frameworks currently use JVM agents to perform bytecode rewriting at run-time. Think of instrumentation agents or bytecode enhancing agents from Object-Relation Mappers such as OpenJPA. In some cases, these transformations can be applied up front at link-time. A jlink plug-in can be a good alternative (or complement) to some of these JVM agent implementations.

对于传统上因为代价太高 JVM 无法即时执行的优化,现在在链接时都是可行的。值得注意的是,jlink 可以优化来自任何模块的代码,无论是应用程序模块还是所使用的库模块或是平台模块。不过,因为 jlink 插件允许任意的字节码重写,所以它们不仅仅只是用来增强性能。目前许多工具和框架都使用 JVM 代理在运行时执行字节码重写,比如来自对象关系映射器(如 OpenJPA)的检测代理或字节码增强代理。在某些情况下,这些转换可以在链接时应用。jlink 插件可以成为某些 JVM 代理实现的一个很好的选择(或补充)。

TIP

Keep in mind, this is the territory of advanced libraries and tools. You are unlikely to write your own jlink plug-in as part of a typical application development process, just as you are unlikely to write your own JVM agent.

请记住,上述功能都是一些高级库和工具所完成的。不太可能将编写自己的 jlink 插件作为典型应用程序开发过程的一部分,就像不太可能编写自己的 JVM 代理一样。

13.8 Cross-Targeting Runtime Images 跨目标运行时映像

A custom runtime image created by jlink runs on only a specific OS and architecture. This is similar to how JDK or JRE distributions differ for Windows, Linux, macOS, and so forth. They contain platform-specific native binaries necessary to run the JVM. You download a Java runtime for your specific OS, and on top of that you run your portable Java applications.

由 jlink 创建的自定义运行时映像仅在特定的操作系统和体系结构上运行,这类似于 JDK 或 JRE 发行版在 Windows、Linux、macOS 等系统上存在的不同之处,这些版本包含运行 JVM 所需的特定于平台的本机二进制文件。可以为特定的操作系统下载 Java 运行时,并运行可移植的 Java 应用程序。

In the examples so far, we’ve built images by using jlink. jlink is part of the JDK (which is platform-specific), but it can create images for different platforms. Doing so is straightforward. Instead of adding the platform modules of the JDK currently running jlink to the module path, you point at the platform modules of the OS and architecture you want to target. Obtaining these alternative platform modules is as easy as downloading the JDK for that platform and extracting it.

在前面的示例中,已经使用 jlink 构建了映像。虽然 jlink 是 JDK(其特定于平台)的一部分,但它可以为不同的平台创建映像,其过程非常简单。不要将当前运行 jlink 的 JDK 的平台模块添加到模块路径,而是指向具体的操作系统和体系结构的平台模块。获得这些替代平台模块就像下载并提取该平台的 JDK 一样简单。

Let’s say we are running on macOS and want to create an image for Windows (32-bit). First, download the correct JDK for 32-bit Windows, and extract it into ~/jdk9-win-32. Then, use the following jlink invocation:

假设我们在 macOS 上运行,并想为 Windows(32 位)创建一个映像。首先,为 32 位 Windows 下载正确的 JDK,并将其解压到~/jdk9-win-32。然后,使用下面的 jlink 调用:

  1. $ jlink --module-path mods/:~/jdk9-win-32/jmods ...

The resulting image contains your application modules from mods bundled with platform modules from the Windows 32-bit JDK. Additionally, the /bin directory of the image contains Windows batch files instead of macOS scripts. All that’s left to do now is distributing the image to the correct target!

生成的映像包含来自 mods 的应用程序模块,而 mods 与来自 Windows 32 位 JDK 的平台模块相绑定。另外,映像的/bin 目录包含了 Windows 批处理文件,而不是 macOS 脚本。现在剩下要做的就是将映像分发到正确的目标上!

WARNING

Runtime images built with jlink don’t update automatically. You are responsible for building updated runtime images when a new Java version is released. Ensure that you distribute only runtime images created from an up-to-date Java version to prevent security issues.

使用 jlink 构建的运行系统映像不会自动更新。当发布新的 Java 版本时,需要构建更新的运行时映像。请确保仅分发根据最新 Java 版本而创建的运行时映像,以防止出现安全问题。

You have seen how jlink can create lean runtime images. When your application is modularized, using jlink is straightforward. Linking is an optional step, which can make a lot of sense when targeting resource-constrained environments.

本章介绍了 jlink 如何创建简洁的运行时映像。当应用程序被模块化时,使用 jlink 是很简单的。虽然链接是一个可选的步骤,但是当面对资源受限的环境时链接能起到很大的作用。