JEP 220:模块化的运行时镜像
概述
重组 JDK 和 JRE 的运行时镜像,以适应模块化并提升性能、安全性和可维护性。定义一种新的 URI 模式,用于命名存储在运行时镜像中的模块、类和资源,同时不暴露镜像的内部结构或格式。根据需要修订现有规范以适应这些更改。
目标
-
为存储的类和资源文件采用一种运行时格式,该格式:
-
比基于古老 ZIP 格式的传统 JAR 格式在时间和空间上更高效;
-
能够按模块定位和加载类及资源文件;
-
能够存储来自 JDK 模块以及库和应用程序模块的类和资源文件;并且
-
可以扩展以容纳更多类型的数据,例如预先计算的 JVM 数据结构和 Java 类的预编译本地代码。
-
-
重组 JDK 和 JRE 的运行时镜像,明确区分开发人员、部署人员和最终用户可以依赖(并在适当情况下修改)的文件,与属于实现内部且可能随时变更的文件。
-
提供受支持的方式来执行常见操作,例如,枚举镜像中的所有类,而当前的做法需要检查运行时镜像的内部结构。
-
实现对当前被授予所有安全权限但实际上并不需要这些权限的 JDK 类进行选择性降权。
-
保留行为良好的应用程序的现有功能,即不依赖于 JRE 和 JDK 运行时镜像内部特性的应用程序。
成功指标
模块化的运行时镜像,等同于 JRE、JDK 和紧凑型配置文件镜像的上一个 JDK 9 构建版本,不得在一组具有代表性的启动、静态占用空间和动态占用空间基准测试中出现性能退化。
非目标
-
保留当前运行时镜像结构的所有方面并不是目标。
-
保留所有现有 API 的完全当前行为并不是目标。
动机
Project Jigsaw 旨在为 Java SE 平台设计并实现一个标准的模块系统,并将该系统应用于平台本身以及 JDK。其主要目标是使平台的实现更容易扩展到小型设备,提升安全性和可维护性,实现更高的应用性能,并为开发者提供更强大的工具以支持大规模编程。
描述
当前运行时镜像结构
JDK 构建系统目前生成两种类型的运行时映像:Java 运行时环境(JRE),它是 Java SE 平台的一个完整实现;以及 Java 开发工具包(JDK),它嵌入了一个 JRE,并包含开发工具和库。(三个 Compact Profile 构建是 JRE 的子集。)
JRE 映像的根目录包含两个目录:bin
和 lib
,其内容如下:
-
bin
目录包含关键的可执行二进制文件,特别是用于启动运行时系统的java
命令。(在 Windows 操作系统中,它还包含运行时系统的动态链接本地库。) -
lib
目录包含各种文件和子目录:
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"
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.so
或libjvm.dylib
,可以被嵌入运行时系统的程序链接。此目录中的其他一些文件也用于外部使用,包括src.zip
和jexec
。 -
lib
目录中的所有其他文件和目录必须被视为运行时系统的私有实现细节。它们不适用于外部使用,并且其名称、格式和内容可能会在不通知的情况下更改。 -
legal
目录包含链接到镜像中的模块的法律声明,每个模块分组到一个子目录中。 -
完整的 JDK 镜像还包含
demo
、man
和include
目录,就像现在一样。(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 文件安装到运行时映像中,这样其内容对于使用该映像编译或在其上运行的每个应用程序都是可见的。
扩展机制是在 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 上运行时,代码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
对象加载存储的类或资源文件的内容。 -
java.security.CodeSource
API 和 安全策略文件 使用 URL 来命名应被授予特定权限的代码库位置。当前,运行时系统中需要特定权限的组件在lib/security/java.policy
文件中通过file
URL 进行标识。例如,椭圆曲线加密提供程序被标识为:file:${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
的例子现在返回的 URL 为:jrt:/java.base/java/lang/Class.class
jrt
协议的内置协议处理器确保了此类URL
对象的getContent
方法能够获取指定类或资源文件的内容。 -
安全策略文件以及其他使用
CodeSource
API 的场景可以使用jrt
URL 来命名特定模块,以实现权限授予的目的。例如,椭圆曲线加密提供程序现在可以通过以下jrt
URL 进行标识:jrt:/jdk.crypto.ec
其他当前被授予所有权限但实际上并不需要这些权限的模块,可以轻松地进行降权处理,即仅授予其实际所需的权限。
-
内置的 NIO 文件系统提供程序 支持
jrt
URL 协议,确保开发工具能够通过加载由 URLjrt:/
命名的 文件系统,枚举并读取运行时镜像中的类和资源文件,如下所示: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-image
和 images/j2re-image
目录分别重命名为 images/jdk
和 images/jre
。
次要规范变更
JEP 162 在 JDK 8 中实现,为 Java SE 平台和 JDK 的模块化工作(如本文及相关 JEP 中所述)进行了多项更改。其中的更改包括移除了要求在运行时镜像的 lib
目录中查找某些配置文件的规范性说明,因为这些文件现在位于 conf
目录中。作为 Java SE 8 的一部分,大多数仅限于 SE 的 API 中的相关说明都已修订,但仍有一些在 Java SE 和 EE 平台之间共享的 API 包含此类说明:
在 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
不再定义,如上所述。假设这些属性具有非空值的现有代码可能无法正常工作。 -
运行时系统的动态链接本地库始终位于
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.jar
和tools.jar
之间分裂,但现在所有这些类都在一个模块中。 -
JRE 镜像中的
bin
目录包含一些以前只在 JDK 镜像中找到的命令,即appletviewer
、idlj
、jrunscript
和jstatd
。与前一项一样,这些更改是包含 API 和工具的组件被模块化的结果。
依赖
本 JEP 是 Project Jigsaw 的四个 JEP 中的第三个。它依赖于 JEP 201,后者将 JDK 源代码重新组织为模块,并升级了构建系统以编译模块。它还依赖于在 JEP 162 中完成的早期准备工作,该工作已在 JDK 8 中实现。