跳到主要内容

JEP 220:模块化的运行时镜像

QWen Max 中英对照

概述

重组 JDK 和 JRE 的运行时镜像,以适应模块化并提升性能、安全性和可维护性。定义一种新的 URI 模式,用于命名存储在运行时镜像中的模块、类和资源,同时不暴露镜像的内部结构或格式。根据需要修订现有规范以适应这些更改。

目标

  • 为存储的类和资源文件采用一种运行时格式,该格式:

    • 比基于古老 ZIP 格式的传统 JAR 格式在时间和空间上更高效;

    • 能够按模块定位和加载类及资源文件;

    • 能够存储来自 JDK 模块以及库和应用程序模块的类和资源文件;并且

    • 可以扩展以容纳更多类型的数据,例如预先计算的 JVM 数据结构和 Java 类的预编译本地代码。

  • 重组 JDK 和 JRE 的运行时镜像,明确区分开发人员、部署人员和最终用户可以依赖(并在适当情况下修改)的文件,与属于实现内部且可能随时变更的文件。

  • 提供受支持的方式来执行常见操作,例如,枚举镜像中的所有类,而当前的做法需要检查运行时镜像的内部结构。

  • 实现对当前被授予所有安全权限但实际上并不需要这些权限的 JDK 类进行选择性降权

  • 保留行为良好的应用程序的现有功能,即不依赖于 JRE 和 JDK 运行时镜像内部特性的应用程序。

成功指标

模块化的运行时镜像,等同于 JRE、JDK 和紧凑型配置文件镜像的上一个 JDK 9 构建版本,不得在一组具有代表性的启动、静态占用空间和动态占用空间基准测试中出现性能退化。

非目标

  • 保留当前运行时镜像结构的所有方面并不是目标。

  • 保留所有现有 API 的完全当前行为并不是目标。

动机

Project Jigsaw 旨在为 Java SE 平台设计并实现一个标准的模块系统,并将该系统应用于平台本身以及 JDK。其主要目标是使平台的实现更容易扩展到小型设备,提升安全性和可维护性,实现更高的应用性能,并为开发者提供更强大的工具以支持大规模编程。

此 JEP 是 Project Jigsaw 的四个 JEP 中的第三个。较早的 JEP 200 定义了模块化 JDK 的结构,而 JEP 201 将 JDK 源代码重新组织为模块。稍后的 JEP 261 引入了实际的模块系统。

描述

当前运行时镜像结构

JDK 构建系统目前生成两种类型的运行时映像:Java 运行时环境(JRE),它是 Java SE 平台的一个完整实现;以及 Java 开发工具包(JDK),它嵌入了一个 JRE,并包含开发工具和库。(三个 Compact Profile 构建是 JRE 的子集。)

JRE 映像的根目录包含两个目录:binlib,其内容如下:

  • bin 目录包含关键的可执行二进制文件,特别是用于启动运行时系统的 java 命令。(在 Windows 操作系统中,它还包含运行时系统的动态链接本地库。)

  • lib 目录包含各种文件和子目录:

    • 各种 .properties.policy 文件,其中大多数可以(但很少)由开发人员、部署人员和最终用户编辑;

    • endorsed 目录,默认情况下不存在,可以将包含 认可标准和独立技术 实现的 JAR 文件放入其中;

    • ext 目录,可以将包含 扩展或可选包 的 JAR 文件放入其中;

    • 各种实现内部的数据文件,采用多种二进制格式,例如字体、色彩配置文件和时区数据;

    • 各种 JAR 文件,包括包含运行时系统的 Java 类和资源文件的 rt.jar

    • Linux、macOS 和 Solaris 操作系统上的运行时系统的动态链接本地库。

JDK 镜像在其 jre 子目录中包含 JRE 的副本,并且包含额外的子目录:

  • bin 目录包含命令行开发和调试工具,例如 javacjavadocjconsole,同时为了方便起见,还包含了 jre/bin 目录中的二进制文件副本;

  • demosample 目录分别包含演示程序和示例代码;

  • man 目录包含 UNIX 风格的手册页;

  • include 目录包含用于编译直接与运行时系统交互的本地代码的 C/C++ 头文件;以及

  • lib 目录包含各种 JAR 文件和其他类型的文件,这些文件组成了 JDK 工具的实现,其中包括 tools.jar,它包含了 javac 编译器的类。

JDK 映像的根目录,或未嵌入在 JDK 映像中的 JRE 映像的根目录,还包含各种 COPYRIGHTLICENSEREADME 文件,以及一个 release 文件,该文件通过简单的键/值属性对来描述映像,例如:

JAVA_VERSION="1.9.0"
OS_NAME="Linux"
OS_VERSION="2.6"
OS_ARCH="amd64"

新的运行时镜像结构

当前 JRE 和 JDK 镜像之间的区别纯粹是历史性的,这是在 JDK 1.2 版本开发后期做出的一项实现决策的结果,并且从未被重新审视过。新的镜像结构消除了这一区别:JDK 镜像只是一个运行时镜像,恰好包含通常在 JDK 中才有的全套开发工具和其他内容。

一个模块化的运行时镜像包含以下目录:

  • bin 目录包含由链接到镜像中的模块定义的任何命令行启动器。(在 Windows 上,它继续包含运行时系统的动态链接本地库。)

  • conf 目录包含 .properties.policy 和其他种类的文件,这些文件旨在由开发人员、部署人员和最终用户编辑,以前位于 lib 目录或其子目录中。

  • 在 Linux、macOS 和 Solaris 上,lib 目录包含运行时系统的动态链接本地库,就像现在一样。这些文件名为 libjvm.solibjvm.dylib,可以被嵌入运行时系统的程序链接。此目录中的其他一些文件也用于外部使用,包括 src.zipjexec

  • lib 目录中的所有其他文件和目录必须被视为运行时系统的私有实现细节。它们不适用于外部使用,并且其名称、格式和内容可能会在不通知的情况下更改。

  • legal 目录包含链接到镜像中的模块的法律声明,每个模块分组到一个子目录中。

  • 完整的 JDK 镜像还包含 demomaninclude 目录,就像现在一样。(samples 目录已被 JEP 298 移除。)

模块化运行时映像的根目录还包含由构建系统生成的 release 文件。为了方便识别运行时映像中存在哪些模块,release 文件包含一个新属性 MODULES,它是一个以空格分隔的模块名称列表。该列表根据模块的依赖关系进行拓扑排序,因此 java.base 模块始终排在第一位。

已移除:认可标准覆盖机制

认可标准覆盖机制 允许在 Java 社区流程之外维护的标准新版本的实现,或者是属于 Java SE 平台但继续独立演进的独立 API 实现,被安装到运行时镜像中。

认可标准机制是根据一个类似路径的系统属性 java.endorsed.dirs 及该属性的默认值 $JAVA_HOME/lib/endorsed 来定义的。一个包含较新版本的认可标准或独立 API 的 JAR 文件,可以通过将其放置在由系统属性指定的目录之一中,或者在系统属性未定义时放置在默认的 lib/endorsed 目录中,从而安装到运行时映像中。这样的 JAR 文件会在运行时被添加到 JVM 的引导类路径的前面,从而覆盖存储在运行时系统中的任何定义。

模块化镜像由模块而不是 JAR 文件组成。今后,通过 可升级模块 的概念,认可标准和独立 API 仅以模块化形式提供支持。因此,我们移除了认可标准覆盖机制,包括 java.endorsed.dirs 系统属性和 lib/endorsed 目录。为了帮助识别此机制的任何现有使用情况,如果设置了该系统属性,或者如果存在 lib/endorsed 目录,编译器和启动器现在都会失败。

已移除:扩展机制

扩展机制 允许将包含扩展 Java SE 平台 API 的 JAR 文件安装到运行时映像中,这样其内容对于使用该映像编译或在其上运行的每个应用程序都是可见的。

机制的定义是基于一个类似路径的系统属性 java.ext.dirs,并且该属性的默认值由 $JAVA_HOME/lib/ext 和一个特定于平台的系统级目录组成(例如,在 Linux 上为 /usr/java/packages/lib/ext)。它的运作方式与认可标准机制非常相似,区别在于放置在扩展目录中的 JAR 文件是由运行时环境的扩展类加载器加载的,这个加载器是引导类加载器的子级,同时也是系统类加载器的父级,而系统类加载器则是从类路径中实际加载要运行的应用程序。因此,扩展类无法覆盖由引导加载器加载的 JDK 类,但它们会优先于由系统加载器及其子级定义的类进行加载。

扩展机制是在 1998 年发布的 JDK 1.2 中引入的,但在现代,我们很少看到它被使用的证据。这并不令人惊讶,因为如今大多数 Java 应用程序会将所需的库直接放在类路径上,而不是要求这些库作为运行时系统的扩展进行安装。

从技术角度来说,虽然在模块化的 JDK 中继续支持扩展机制是可能的,但会显得笨拙。为了简化 Java SE 平台和 JDK,我们移除了扩展机制,包括 java.ext.dirs 系统属性和 lib/ext 目录。为了帮助识别该机制的任何现有使用情况,如果设置了此系统属性,或者如果存在 lib/ext 目录,编译器和启动器现在都会报错。编译器和启动器默认忽略特定于平台的系统级扩展目录,但如果指定了 -XX:+CheckEndorsedAndExtDirs 命令行选项,则当该目录存在且不为空时,它们会报错。

由于扩展机制相关的几个特性本身很有用,因此被保留下来:

  • Class-Path 清单属性,它指定了另一个 JAR 文件所需的 JAR 文件;

  • {Specification,Implementation}-{Title,Version,Vendor} 清单属性,它们指定包和 JAR 文件的版本信息;

  • Sealed 清单属性,它密封一个包或 JAR 文件;以及

  • 扩展类加载器本身,尽管现在它被称为 平台 类加载器

移除:rt.jartools.jar

以前存储在 lib/rt.jarlib/tools.jarlib/dt.jar 以及各种其他内部 JAR 文件中的类和资源文件,现在以一种更高效的格式存储在 lib 目录下特定于实现的文件中。这些文件的格式并未指定,并且可能会在不另行通知的情况下发生变化。

移除 rt.jar 和类似的文件导致了三个不同的问题:

  1. 现有的标准 API,例如 ClassLoader::getSystemResource 方法,会返回 URL 对象,用于命名运行时镜像中的类和资源文件。例如,在 JDK 8 上运行时,代码

    ClassLoader.getSystemResource("java/lang/Class.class");

    返回一个 jar URL,形式如下:

    jar:file:/usr/local/jdk8/jre/lib/rt.jar!/java/lang/Class.class

    可以看到,该 URL 嵌入了一个 file URL,用于命名运行时镜像中的实际 JAR 文件。该 URL 对象的 getContent 方法可以通过内置的 jar URL 协议处理器来检索类文件的内容。

    模块化镜像不包含任何 JAR 文件,因此上述形式的 URL 没有意义。幸运的是,getSystemResource 和相关方法的规范并不要求这些方法返回的 URL 对象必须使用 JAR 协议。然而,它们确实要求能够通过这些 URL 对象加载存储的类或资源文件的内容。

  2. java.security.CodeSource API 和 安全策略文件 使用 URL 来命名应被授予特定权限的代码库位置。当前,运行时系统中需要特定权限的组件在 lib/security/java.policy 文件中通过 file URL 进行标识。例如,椭圆曲线加密提供程序被标识为:

    file:${java.home}/lib/ext/sunec.jar

    显然,这在模块化镜像中是没有意义的。

  3. 集成开发环境 (IDE) 和其他类型的开发工具需要能够枚举存储在运行时镜像中的类和资源文件,并读取其内容。如今,它们通常通过直接打开和读取 rt.jar 和类似的文件来实现这一点。当然,在模块化镜像中这是不可能的。

用于命名存储模块、类和资源的新 URI 方案

为了解决上述三个问题,可以使用一种新的 URL 模式 jrt,用于命名存储在运行时镜像中的模块、类和资源,而无需暴露该镜像的内部结构或格式。

jrt URL 是一个分层的 URI,符合 RFC 3986 的语法规范。

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

其中 $MODULE 是可选的模块名称,而 $PATH 如果存在,则是该模块内特定类或资源文件的路径。jrt URL 的含义取决于其结构:

  • jrt:/$MODULE/$PATH 指的是给定 $MODULE 中名为 $PATH 的特定类或资源文件。

  • jrt:/$MODULE 指的是模块 $MODULE 中的所有类和资源文件。

  • jrt:/ 指的是当前运行时映像中存储的所有类和资源文件的整个集合。

这三种形式的 jrt URL 通过以下方式解决了上述问题:

  1. 当前返回 jar URL 的 API 现在将返回 jrt URL。上述调用 ClassLoader::getSystemResource 的例子现在返回的 URL 为:

    jrt:/java.base/java/lang/Class.class

    jrt 协议的内置协议处理器确保了此类 URL 对象的 getContent 方法能够获取指定类或资源文件的内容。

  2. 安全策略文件以及其他使用 CodeSource API 的场景可以使用 jrt URL 来命名特定模块,以实现权限授予的目的。例如,椭圆曲线加密提供程序现在可以通过以下 jrt URL 进行标识:

    jrt:/jdk.crypto.ec

    其他当前被授予所有权限但实际上并不需要这些权限的模块,可以轻松地进行降权处理,即仅授予其实际所需的权限。

  3. 内置的 NIO 文件系统提供程序 支持 jrt URL 协议,确保开发工具能够通过加载由 URL jrt:/ 命名的 文件系统,枚举并读取运行时镜像中的类和资源文件,如下所示:

    FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
    byte[] jlo = Files.readAllBytes(fs.getPath("modules", "java.base",
    "java/lang/Object.class"));

    此文件系统中的顶级 modules 目录包含运行时镜像中每个模块的一个子目录。顶级 packages 目录包含运行时镜像中每个包的一个子目录,而该子目录包含一个指向定义该包的模块子目录的符号链接。

    对于支持 JDK 9 代码开发但自身运行在 JDK 8 上的工具,JDK 9 运行时镜像的 lib 目录中放置了一个适用于 JDK 8 的此文件系统提供程序副本,文件名为 jrt-fs.jar

jrt URL 协议处理器不会为第二和第三种形式的 URL 返回任何内容。)

构建系统更改

构建系统使用 Java 链接器(JEP 282)生成上述新的运行时镜像格式。

我们借此机会,最终将 images/j2sdk-imageimages/j2re-image 目录分别重命名为 images/jdkimages/jre

次要规范变更

JEP 162 在 JDK 8 中实现,为 Java SE 平台和 JDK 的模块化工作(如本文及相关 JEP 中所述)进行了多项更改。其中的更改包括移除了要求在运行时镜像的 lib 目录中查找某些配置文件的规范性说明,因为这些文件现在位于 conf 目录中。作为 Java SE 8 的一部分,大多数仅限于 SE 的 API 中的相关说明都已修订,但仍有一些在 Java SE 和 EE 平台之间共享的 API 包含此类说明:

  • javax.xml.stream.XMLInputFactory 指定了 ${java.home}/lib/stax.properties (JSR 173)。

  • javax.xml.ws.spi.Provider 指定了 ${java.home}/lib/jaxws.properties (JSR 224)。

  • javax.xml.soap.MessageFactory 及相关类指定了 ${java.home}/lib/jaxm.properties (JSR 67)。

在 Java SE 9 中,这些语句不再强制要求 lib 目录。

测试

一些现有的测试直接使用了运行时镜像的内部结构(例如,rt.jar)或引用了已不存在的系统属性(例如,java.ext.dirs)。这些测试已经被修复。

在模块系统开发期间,始终可以获取包含此处所述更改的早期访问版本。强烈鼓励 Java 社区成员针对这些版本测试他们的工具、库和应用程序,以帮助识别兼容性问题。

风险与假设

该提案的核心风险在于兼容性,具体总结如下:

  • 如上所述,JDK 镜像不再包含 jre 子目录。假设该目录存在的现有代码可能无法正常工作。

  • 如上所述,JDK 和 JRE 镜像不再包含文件 lib/rt.jarlib/tools.jarlib/dt.jar 以及其他内部 JAR 文件。假设这些文件存在的现有代码可能无法正常工作。

  • 系统属性 java.endorsed.dirsjava.ext.dirs 不再定义,如上所述。假设这些属性具有非空值的现有代码可能无法正常工作。

  • 运行时系统的动态链接本地库始终位于 lib 目录中,Windows 除外;在 Linux 和 Solaris 构建中,它们以前被放置在 lib/$ARCH 子目录中。这是可以支持多种 CPU 架构的镜像遗留下来的残余物,这已经不再是需求。

  • src.zip 文件现在位于 lib 目录而不是顶层目录,并且此文件现在包括镜像中的每个模块的一个目录。读取此文件的 IDE 和其他工具需要更新。

  • 返回 URL 对象以命名运行时镜像内的类和资源文件的现有标准 API 现在返回 jrt URL,如上所述。期望这些 API 返回 jar URL 的现有代码可能无法正常工作。

  • 内部系统属性 sun.boot.class.path 已被移除。依赖此属性的现有代码可能无法正常工作。

  • JDK 镜像中的类和资源文件以前在 lib/tools.jar 中找到,并且仅当该文件被添加到类路径时才可见,现在通过系统类加载器或在某些情况下通过引导类加载器可见。包含这些文件的模块未提及在应用程序类路径中,即系统属性 java.class.path 的值中。

  • 以前在 lib/dt.jar 中找到并且仅当该文件被添加到类路径时才可见的类和资源文件现在通过引导类加载器可见,并且存在于 JRE 和 JDK 中。

  • 以前在 lib 目录中找到的配置文件,包括安全策略文件,现在位于 conf 目录中。检查或操作这些文件的现有代码可能需要更新。

  • 某些现有包中的类型定义类加载器已更改。对这些类型的类加载器做出假设的现有代码可能无法正常工作。具体更改在 JEP 261 中列举。其中一些更改是包含 API 和工具的组件被模块化的结果。这样一个组件的类历史上在 rt.jartools.jar 之间分裂,但现在所有这些类都在一个模块中。

  • JRE 镜像中的 bin 目录包含一些以前只在 JDK 镜像中找到的命令,即 appletvieweridljjrunscriptjstatd。与前一项一样,这些更改是包含 API 和工具的组件被模块化的结果。

依赖

本 JEP 是 Project Jigsaw 的四个 JEP 中的第三个。它依赖于 JEP 201,后者将 JDK 源代码重新组织为模块,并升级了构建系统以编译模块。它还依赖于在 JEP 162 中完成的早期准备工作,该工作已在 JDK 8 中实现。