Skip to content

JEP 139: Enhance javac to Improve Build Speed | 增强 javac 以提高构建速度

总结

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

目标

最高级别的目标是:

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

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

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

非目标

该项目仅关注 javac 和新的包装程序所需的更改。它不涵盖 JDK Makefile 中为了利用这些更改而需要的更改;这些更改在 JEP 138:基于 Autoconf 的构建系统 中进行了描述。

该项目不会关注 javac 中为了加快 Javadoc 生成速度而需要的更改。

成功指标

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

分布在不同核心上的工作负载将不会完全平衡,并且已知相同的工作将被多次重新计算。但是,本 JEP 支持的更改将使我们能够逐步改进 javac 中核心之间的工作共享。

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

在执行增量构建并删除类或本地方法后,输出目录应干净无残留;即,不应保留与已删除源代码相对应的类或 C 头文件。

动机

完整构建 OpenJDK 的速度过慢,这给开发人员和构建系统带来了额外的负担。因此,开发人员只签出并构建源代码的一部分,因为整个产品的构建时间过长。

描述

内部 smart-javac 包装程序可能会被这样调用:

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

这将使用所有(-j)核心编译在 sourcepath 中找到的、包名与任何内容('*')匹配的所有源文件。将在(-d)outputdir 中创建一个名为 .javac_state 的数据库文件,该文件将包含执行快速增量编译所需的所有信息,包括正确清理消失的类和 C 头文件以及正确的依赖跟踪。

smart-javac 包装程序通过为每个核心创建一个 JavaCompiler 实例来实现多核支持。然后,将待编译的源代码拆分成包,并随机分配给 JavaCompilers。如果随机选择的包有依赖项,则这些依赖项将自动编译,但不会写入磁盘,即使用 -Ximplicit:none。如果稍后请求了作为随机选择的包的一部分的隐式编译依赖项,则不会浪费隐式工作;相反,已编译的依赖项将被写入磁盘。

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

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

对于包含本地方法的任何类,将自动运行 javah,并将生成的 C 头文件放在(-h)headerdir 中。一个新的注解 @ForceNativeHeader 用于那些有最终静态原始类型需要导出到 JNI,但没有本地方法的类。

为了避免重新启动 javac 并丢失 JVM 所做的优化,smart-javac 包装程序支持 -server 选项。此选项将启动一个后台 javac 服务器,以便每个后续引用相同 portfile 的 smart-javac 包装程序调用都将重用相同的服务器。

-server 参数包括:

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

示例:

shell
-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 是存储在 portfile 中的 64 位随机整数。portfile 具有典型的临时文件权限,即只有所有者才能从中读取或写入。

替代方案

我们不必让 javac 真正并行化,而是可以并行启动多个单线程的、针对不同且独立的 Java 包的编译。这样做不需要对 javac 进行任何更改,但会使 Makefile 的正确性校验更加困难,且速度提升不会很大。

测试

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

新选项都不会公开,因此无需向 javac 测试套件中添加测试,但会在 langtools 测试目录中为 smart-javac 包装器添加更具体的测试。

风险和假设

生成的产品不正确

  • 风险:Javac 的更改可能导致生成不正确的位
  • 缓解计划:正确测试生成的构建

旧版 Makefile 将与新 Makefile 同时提供,以简化新旧位的比较。

依赖

此 JEP 不依赖于任何其他更改。它是 JEP 138: 基于自动配置的构建系统 的基础,后者将使用 javac 的这一新功能来加速 JDK 的构建。新 Makefile 可以使用未经修改的 javac,但除非完成此 JEP,否则它们无法实现预期的加速效果和增量构建支持。

影响

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