JEP 139:增强 javac 以提高构建速度
概括
通过修改 Java 编译器以在单个持久进程中的所有可用内核上运行、跟踪构建之间的包和类依赖性、自动生成本机方法的头文件以及清理类和类,减少构建 JDK 所需的时间并启用增量构建。不再需要的头文件。
目标
最高层目标是:
- 通过让 javac 使用所有核心并在服务器进程中重用 javac 来提高构建速度。
- 通过增量构建 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 包装器可能会像这样调用:
$ java -jar sjavac.jar -classpath ... -sourcepath ... -pkg '*' \
-j all -h headerdir -d outputdir
'*'
这将使用所有 ( ) 核心编译在源路径中找到的所有源文件,其包名称与任何内容 ( ) 匹配-j
。将在-d
调用的 () 输出目录中创建一个数据库文件.javac_state
,该文件将包含进行快速增量编译所需的所有信息,并正确清理消失的类和 C 标头以及正确的依赖项跟踪。
smart-javac 包装器通过为每个核心创建一个 JavaCompiler 实例来实现多核心支持。然后将要编译的源代码分成包并随机分发给 Java 编译器。如果随机选择的包有依赖项,那么这些依赖项将被自动编译,但不会写入磁盘,即, -Ximplicit:none
。如果稍后请求隐式编译 的依赖项作为随机选择的包的一部分,则隐式工作不会浪费;相反,已编译的依赖项将写入磁盘。
由于初始编译不知道包依赖于哪些包,因此随机分配工作是我们能做的最好的事情。因此,java.lang.Object
有多少个核心就会被重新编译多少次,但只有一个 JavaCompiler 负责写入java.lang.Object
磁盘。足够多的包彼此独立,使之成为利用多核的可行策略。
当我们将来改进javac(不属于这个JEP的一部分)时,Java编译器之间将分担越来越多的工作,最终我们将达到java.lang.Object
只编译一次的状态。
javah 将自动在任何包含本机方法的类上运行,生成的 C 头文件将放入 ( -h
) headerdir 中。新的注释@ForceNativeHeader
用于具有需要导出到 JNI 的最终静态基元但没有本机方法的类。
为了避免重新启动 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端口;端口号存储在端口文件中。
- 仅允许来自本地主机的连接。
- 在进行任何编译之前,需要向服务器提供唯一的 cookie。
cookie 是存储在端口文件中的 64 位随机整数。端口文件具有典型的临时文件权限,_即_仅允许所有者对其进行读取或写入。
备择方案
我们可以并行启动多个不同且独立的 java 包的单线程编译,而不是让 javac 正确并行。这不需要对 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 选项编写单独的测试用例。