JEP 220: Modular Run-Time Images | 模块化运行时映像
摘要
重构 JDK 和 JRE 运行时映像以适应模块,并提高性能、安全性和可维护性。定义一种新的 URI 方案,用于命名运行时映像中存储的模块、类和资源,而不暴露映像的内部结构或格式。根据需要修改现有规范以适应这些变化。
目标
- 采用存储类和资源文件的运行时格式,该格式:
- 比传统的基于古老 ZIP 格式的 JAR 格式更高效(时间和空间);
- 可以按模块定位和加载类和资源文件;
- 可以存储来自 JDK 模块、库和应用程序模块的类和资源文件;
- 可以扩展以适应未来的其他类型数据,例如预计算的 JVM 数据结构和 Java 类的预编译本机代码。
- 重构 JDK 和 JRE 运行时映像,明确区分开发人员、部署者和最终用户可以依赖并在适当情况下修改的文件与实现的内部文件,后者可能随时变更。
- 提供支持常见操作的方式,例如枚举映像中存在的所有类,而无需检查运行时映像的内部结构。
- 使得今天被授予所有安全权限但实际上不需要这些权限的 JDK 类能够进行选择性的取消特权处理。
- 保留良好行为的现有应用程序,即不依赖 JRE 和 JDK 运行时映像的内部细节的应用程序。
成功指标
基于代表性的一组启动、静态占用空间和动态占用空间基准测试,模块化的运行时映像与前一个 JDK 9 构建版本的 JRE、JDK 和 Compact Profile 映像相当,不得存在退化。
非目标
- 不保留当前运行时映像结构的所有方面。
- 不保留所有现有 API 的精确当前行为。
动机
Project Jigsaw 旨在为 Java SE 平台设计和实现一个标准的模块系统,并将该系统应用于平台本身和 JDK。其主要目标是使平台的实现更容易在小型设备上扩展,提高安全性和可维护性,提高应用程序性能,并为开发人员提供更好的大规模编程工具。
该 JEP 是 Project Jigsaw 的四个 JEP 中的第三个。之前的 JEP 200 定义了模块化 JDK 的结构,JEP 201 则将 JDK 源代码重新组织为模块。稍后的 JEP 261 引入了实际的模块系统。
描述
当前运行时映像结构
JDK 构建系统目前生成两种类型的运行时映像:Java Runtime Environment (JRE),它是 Java SE 平台的完整实现;以及 Java Development Kit (JDK),它嵌入了一个 JRE,并包括开发工具和库(三个 Compact Profile 版本是 JRE 的子集)。
JRE 映像的根目录包含两个目录:bin
和 lib
,其内容如下:
bin
目录包含必要的可执行二进制文件,尤其是用于启动运行时系统的java
命令(在 Windows 操作系统上,它还包含运行时系统的动态链接本机库)。lib
目录包含各种文件和子目录:- 各种
.properties
和.policy
文件,大多数可以由开发人员、部署者和最终用户编辑,但很少这样做。 endorsed
目录,默认情况下不存在,可以放置包含认可的标准和独立技术实现的 JAR 文件。ext
目录,可以放置包含扩展或可选包的 JAR 文件。- 各种以二进制格式存储的实现内部数据文件,例如字体、颜色配置文件和时区数据。
- 各种 JAR 文件,包括
rt.jar
,其中包含运行时系统的 Java 类和资源文件。 - Linux、macOS 和 Solaris 操作系统上的运行时系统动态链接本机库。
- 各种
JDK 映像在其 jre
子目录中包含了一个 JRE 的副本,并包含其他子目录:
bin
目录包含命令行开发和调试工具,如javac
、javadoc
和jconsole
,还包含jre/bin
目录中二进制文件的副本,以方便使用。demo
和sample
目录分别包含演示程序和示例代码。man
目录包含类 Unix 手册页面。include
目录包含用于编译与运行时系统直接交互的本机代码的 C/C++ 头文件。lib
目录包含各种 JAR 文件和其他类型的文件,包括 JDK 工具的实现,其中包括tools.jar
,其中包含javac
编译器的类。
JDK 映像的根目录(或不嵌入在 JDK 映像中的 JRE 映像的根目录)还包含各种 COPYRIGHT
、LICENSE
和 README
文件,以及一个 release
文件,以键值对(例如 JAVA_VERSION="1.9.0"
、OS_NAME="Linux"
等)的形式描述映像。
新的运行时镜像结构
目前 JRE 和 JDK 镜像之间的区别纯粹是历史原因,是在 JDK 1.2 版本开发的晚期做出的实现决策,并且从未重新审视过。新的镜像结构消除了这种区别:JDK 镜像只是一个包含了完整的开发工具和其他历史上在 JDK 中找到的内容的运行时镜像。
模块化的运行时镜像包含以下目录:
bin
目录包含镜像链接到的模块定义的任何命令行启动器。(在 Windows 上,它仍然包含运行时系统的动态链接本地库。)conf
目录包含由开发人员、部署者和最终用户编辑的.properties
、.policy
和其他类型的文件,以前这些文件可以在lib
目录或其子目录中找到。在 Linux、macOS 和 Solaris 上,
lib
目录包含运行时系统的动态链接本地库,与当前版本相同。这些文件的名称为libjvm.so
或libjvm.dylib
,可能会被嵌入运行时系统的程序链接。该目录中的一些其他文件也供外部使用,包括src.zip
和jexec
。lib
目录中的所有其他文件和目录必须被视为运行时系统的私有实现细节。它们不适用于外部使用,其名称、格式和内容可能会随时更改。legal
目录包含链接到镜像中的模块的法律声明,每个模块分组在一个子目录中。完整的 JDK 镜像还包含
demo
、man
和include
目录,与当前版本相同。(samples
目录已被 JEP 298 移除。)
模块化运行时镜像的根目录还包含由构建系统生成的 release
文件。为了方便判断运行时镜像中存在哪些模块,release
文件包含一个新的属性 MODULES
,它是一个由这些模块名称组成的以空格分隔的列表。该列表根据模块的依赖关系进行拓扑排序,因此 java.base
模块总是排在第一位。
移除:标准认可覆盖机制
标准认可覆盖机制允许在 Java Community Process 之外维护的较新版本标准或独立 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.jar
和 tools.jar
之前存储在 lib/rt.jar
、lib/tools.jar
、lib/dt.jar
和其他内部 JAR 文件中的类和资源文件现在以更高效的格式存储在 lib
目录下的特定实现文件中。这些文件的格式未指定,并且可能会在不提前通知的情况下进行更改。
移除 rt.jar
和类似的文件导致了三个不同的问题:
现有的标准 API,如
ClassLoader::getSystemResource
方法返回URL
对象以命名运行时镜像内的类和资源文件。例如,在 JDK 8 上运行以下代码javaClassLoader.getSystemResource("java/lang/Class.class");
将返回一个形式为
javajar:file:/usr/local/jdk8/jre/lib/rt.jar!/java/lang/Class.class
的
jar
URL,可以看到其中嵌入了一个用于命名运行时镜像内实际 JAR 文件的file
URL。模块化镜像不包含任何 JAR 文件,因此上述形式的 URL 没有意义。幸运的是,getSystemResource
和相关方法的规范并不要求这些方法返回的URL
对象实际上使用 JAR 方案。但它们确实要求可以通过这些URL
对象加载存储的类或资源文件的内容。java.security.CodeSource
API 和安全策略文件使用 URL 来命名需要授予特定权限的代码库位置。目前需要特定权限的运行时系统组件在lib/security/java.policy
文件中通过file
URL 进行标识。例如,椭圆曲线密码提供程序被标识为bashfile:${java.home}/lib/ext/sunec.jar
在模块化镜像中,这显然没有意义。
集成开发环境(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 解决了上述问题:
目前返回
jar
URL 的 API 现在返回jrt
URL。例如,上述调用ClassLoader::getSystemResource
现在返回 URLjavajrt:/java.base/java/lang/Class.class
jrt
方案的内置协议处理器确保这些URL
对象的getContent
方法检索命名类或资源文件的内容。安全策略文件和其他使用
CodeSource
API 的地方可以使用jrt
URL 来命名特定模块,以授予权限。例如,椭圆曲线密码提供程序现在可以通过jrt
URL 进行标识:bashjrt:/jdk.crypto.ec
其他当前被授予所有权限但实际上并不需要的模块可以简单地取消特权,即仅给予它们所需的权限。
内置的NIO 文件系统提供程序用于
jrt
URL 方案,确保开发工具可以通过加载由 URLjrt:/
命名的 FileSystem 来枚举和读取运行时镜像中的类和资源文件,如下所示:javaFileSystem 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
目录中名为jrt-fs.jar
的文件中的此文件系统提供程序的副本。
(jrt
URL 协议处理器对于第二种和第三种形式的 URL 不返回任何内容。)
构建系统更改
构建系统使用 Java 链接器(JEP 282)生成上述新的运行时镜像格式。
我们在这里有了机会,最终将images/j2sdk-image
和images/j2re-image
目录重命名为分别为images/jdk
和images/jre
。
较小的规范更改
JEP 162 在 JDK 8 中实现,对准备 Java SE 平台和 JDK 进行模块化工作的一些更改进行了调整。其中一些更改是删除了规范说明中对某些配置文件在运行时镜像的lib
目录中查找的规定,因为这些文件现在位于conf
目录下。大多数具有此类规定的仅限于 SE 的 API 已经在 Java SE 8 的一部分进行了修改,但在 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.jar
、lib/tools.jar
、lib/dt.jar
和其他内部 JAR 文件,如前所述。现有代码如果假设这些文件存在可能无法正常工作。如前所述,不再定义系统属性
java.endorsed.dirs
和java.ext.dirs
。现有代码如果假设这些属性具有非null
值可能无法正常工作。运行时系统的动态链接本地库始终位于
lib
目录中,Windows 除外;在 Linux 和 Solaris 构建中,它们以前放置在lib/$ARCH
子目录中。这是支持多个 CPU 架构的图像的残余遗迹,这已不再是必需的。src.zip
文件现在位于lib
目录而不是顶级目录中,并且此文件现在包含图像中每个模块的一个目录。需要更新读取此文件的 IDE 和其他工具。现有的标准 API 返回
URL
对象来命名运行时图像中的类和资源文件,如前所述,现在返回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.jar
和tools.jar
中,但现在所有这些类都在一个单独的模块中。JRE 图像中的
bin
目录包含了一些以前仅在 JDK 图像中才存在的命令,即appletviewer
、idlj
、jrunscript
和jstatd
。与前一项类似,这些更改是由于同时包含 API 和工具的组件的模块化方式导致的。
依赖关系
这个 JEP 是项目拼图的四个 JEP 中的第三个。它依赖于JEP 201,该 JEP 将 JDK 源代码重新组织为模块并升级了构建系统以编译模块。它还依赖于在 JDK 8 中实施的JEP 162中进行的早期准备工作。