跳到主要内容

JEP 139:增强 javac 以提高构建速度

QWen Max 中英对照

概述

通过修改 Java 编译器,使其在单个持久进程中运行于所有可用核心上,跟踪构建之间的包和类依赖关系,自动生成本地方法的头文件,并清理不再需要的类和头文件,从而减少构建 JDK 所需的时间并启用增量构建。

目标

顶级目标是:

  1. 通过让 javac 使用所有核心并在服务器进程中重用 javac,提高构建速度。
  2. 通过让 javac 增量构建,简化开发人员的工作。

这个项目是改进 JDK 构建基础设施的更大努力的一部分。

javac 的改进将是内部的,无法通过 javac 启动器的公共 API 使用。相反,一个新的内部包装程序,称为 smart-javac(或简称为 sjavac),将会承载这些新功能。最终,当 javac 包装器的功能稳定后,可以通过未来的 JEP(Java 改进提案)提议将其移至 javac 的公共 API 中。这将允许所有 Java 开发人员利用这些改进。

非目标

该项目仅涉及 javac 和新的包装器中所需的更改。它不包括 JDK Makefile 中为利用这些更改而需要进行的修改;相关内容在 JEP 138:基于 Autoconf 的构建系统 中进行了描述。

该项目不会涉及加快 Javadoc 生成所需的 javac 更改。

成功指标

编译 Java 源代码时应使用所有核心,并且在使用多个核心时,构建性能应有所提升。

分配在不同核心上的工作负载将无法做到完美平衡,而且众所周知,同样的工作会被重复计算多次。但是,此 JEP 支持的变更将使我们能够逐步改进 javac 中核心之间的工作共享。

增量构建应该只重新编译已更改的包及其依赖项。

在完成增量构建后,如果某些类或本地方法被移除了,输出目录应该清理干净;也就是说,不应该保留与已移除源代码相对应的任何类或 C 头文件。

动机

构建完整的 OpenJDK 过程过于缓慢,这给开发者和构建系统带来了额外的负担。因此,开发者通常只会检出并构建部分源代码,因为整个产品的构建耗时过长。

描述

内部的 smart-javac 包装器可能会像这样被调用:

$ java -jar sjavac.jar -classpath ... -sourcepath ... -pkg '*' \
-j all -h headerdir -d outputdir

这将会使用所有 (-j) 核心编译在 sourcepath 中找到的所有包名匹配任意名称('*')的源文件。一个数据库文件将会在 (-d) 输出目录中创建,名为 .javac_state,其中包含进行快速增量编译所需的所有信息,包括对消失的类和 C 头文件的正确清理以及正确的依赖关系跟踪。

智能 javac 包装器通过为每个核心创建一个 JavaCompiler 实例来实现多核支持。随后,待编译的源代码被分割成多个包,并随机分配给各个 JavaCompiler。如果某个随机选择的包存在依赖关系,则这些依赖会被自动编译,但不会写入磁盘,即使用 -Ximplicit:none 选项。如果某个隐式编译的依赖项后来作为随机选择的包的一部分被请求,则隐式完成的工作不会浪费;相反,已经编译好的依赖项将被写入磁盘。

由于初始编译无法知道一个包依赖于哪些包,因此随机分配工作是我们能做到的最好的方法。因此,java.lang.Object 会被重新编译多次,次数等于核心数,但只有一个 JavaCompiler 负责将 java.lang.Object 写入磁盘。有足够的包彼此独立,这使得利用多核成为一个可行的策略。

未来当我们改进 javac 时(不是本 JEP 的一部分),JavaCompilers 之间将共享越来越多的工作,最终我们将达到 java.lang.Object 只被编译一次的状态。

对于包含本地方法的任何类,javah 将自动运行,并且生成的 C 头文件将被放置在 (-h) 指定的头文件目录中。对于那些需要导出到 JNI 的具有 final static 基本类型的类(但没有本地方法),使用了一个新的注解 @ForceNativeHeader

为了避免重新启动 javac 并丢失 JVM 所做的优化,smart-javac 包装器支持一个 -server 选项。此选项将生成一个后台的 javac 服务器,这样随后每次调用引用相同端口文件的 smart-javac 包装器时,都会重用同一个服务器。

-server 参数为:

  • portfile = 用于存储 TCP 端口的位置
  • logfile = 用于存储 javac 输出的位置,默认为 portfile+".logfile"
  • stdouterrfile = 用于存储服务器输出的位置,默认为 portfile+".stdouterr"
  • javac = 服务器启动的 javac 路径,其中空格和逗号分别替换为 %20%2C

示例:

-server:portfile=/tmp/jdk.port,javac=/usr/local/bin/java%20\
-jar%20/tmp/openjdk/langtools/dist/lib/bootstrap/sjavac.jar

由于 javac 当前无法在并发编译之间共享状态,因此每个额外的核心将消耗大约与单次调用 javac 相同的内存量。在此 JEP 完成后,核心之间的改进共享将会逐步引入。可以使用 -j 选项来限制核心的数量,从而限制内存使用。

javac 服务器会在所有编译完成后保持在内存中。服务器将在 30 秒不活动后自动关闭,并释放内存和其他资源。

服务器以与普通 javac 编译器相同的用户身份运行,因此拥有相同的权限,并且能够写入构建输出目录。与普通的 javac 编译器不同,编译可以通过 TCP 端口连接到服务器来触发。只有命令行(而不是源代码)会通过 TCP 发送。

一个潜在的安全风险是,攻击者可能会添加某些恶意代码片段的编译内容,这些内容会出现在输出目录中。为了减轻这种风险,我们将采取以下几种措施:

  • 每次打开一个新的 TCP 端口;端口号存储在 portfile 中。
  • 仅允许来自本地主机的连接。
  • 在任何编译发生之前,要求向服务器出示一个唯一的 cookie。

Cookie 是存储在端口文件中的 64 位随机整数。端口文件具有典型的临时文件权限,即只有所有者才被允许从中读取或写入。

替代方案

与其正确地并行化 javac,我们可以并行启动几个不同且独立的 Java 包的单线程编译。这不需要对 javac 做任何更改,但要确保 Makefile 的正确性会困难得多,而且速度提升也不会那么显著。

测试

构建基础设施项目将测试旧构建系统和新构建系统的输出是否相同。这将确保 smart-javac 包装器为相同的源文件生成完全相同的输出。

所有新的选项都不会公开,因此不需要向 javac 测试套件中添加测试,但针对 smart-javac 包装器的更具体测试将会添加到 langtools 测试目录中。

风险与假设

结果产品不正确

  • 风险:Javac 的更改会导致构建出错误的二进制文件
  • 缓解计划:对生成的构建进行充分测试

旧的构建 makefile 将与新的 makefile 同时提供,以简化旧版本和新版本之间的比较。

依赖

该 JEP 不依赖于任何其他变更。它为 JEP 138:基于 Autoconf 的构建系统 奠定了基础,后者将使用 javac 中的这一新功能来加速 JDK 的构建。新的 makefile 可以使用未经修改的 javac,但除非完成此 JEP,否则无法实现预期的加速和增量构建支持。

影响

  • 兼容性:影响较低。我们不会为 javac 添加新的参数。
  • 安全性:影响较低。在描述部分,有关于为编译任务开放服务器的安全性讨论。
  • 国际化/本地化 (I18n/L10n):新的 smart-javac 包装器功能可能会导致一些新消息的出现;由于 smart-javac 尚未成为任何公共 API 的一部分,这些消息将不会被完全翻译。
  • 测试:除了构建本身之外,还应为不同的 smart-javac 选项编写单独的测试用例。