sjavac浅析

sjavac(smarter java compilation)最早在openjdk8中提供了初级版本,其初衷是用来加速jdk自己的编译。在9中进行过一版优化,使其更加稳定可靠,能够用来编译任意的大型java项目。
java8 backport  http://sjavac.s3-website-eu-west-1.amazonaws.com/ 感兴趣可以试用该版本
其实际上是一个包装在原始javac外部的wrapper实现。
sjavac在javac的基础上实现了:
多线程编译(javac是单线程的),利用多核并行加速编译.
提供一个tcp server模式,暴露编译服务. 利用jit编译加速编译.
基于时间戳+pubapi比较的方式来实施增量编译
在java编译领域,gradle和bazel也采取了类似的思路来加速编译,实现增量(bazel和gradle的增量检测更严谨,其采取文件内容摘要而非时间戳来判断文件是否修改),且已投入工业界被大量使用。总的来说,sjavac并不算是非常新颖或先进的技术,开发能直接使用sjavac的场景也很少,ide方面倒是可以集成sjavac来优化其编译速度。
从实际测试效果来看,sjavac的编译速度由于多核并行确实会有较大提升。接下来,我们看看sjavac中比较有趣的地方是如何实现的。
1.首先我们看看其是如何实现并行编译的
a. 在sjavac8中,多线程编译实现比较简单。客户端将待编译的文件按包分组,同一个包总是由同一个javac实例编译,多个不同的包可以由同一个javac实例编译,每个组分别发送到服务端请求编译。并使用-implicit:none参数禁止输出隐式编译产物,从而在每个核心上进行完全独立的编译。这种并行方式,源文件之间若存在依赖的话,并行度不会受到影响,但同一个源文件可能会进行多次重复编译,cpu使用率较低,且总耗时的收益会受到影响。极端情况下,n个链式依赖的源文件,编译时间不比单线程更快,却可能有n-1个编译任务都是完全冗余的。
for (String pkgName : packageNames) {
CompileChunk cc = compileChunks[ci];
Set<URI> s = pkgSrcs.get(pkgName);
if (cc.srcs.size()+s.size() > sourcesPerCompile && ci < numCompiles-1) {
from = null;
ci++;
cc = compileChunks[ci];
}
cc.numPackages++;
cc.srcs.addAll(s);

// Calculate nice package names to use as information when compiling.
String justPkgName = Util.justPackageName(pkgName);
// Fetch how many packages depend on this package from the old build state.
Set<String> ss = oldPackageDependents.get(pkgName);
if (ss != null) {
// Accumulate this information onto this chunk.
cc.numDependents += ss.size();
}
if (from == null || from.trim().equals("")) from = justPkgName;
cc.pkgNames.append(justPkgName+"("+s.size()+") ");
cc.pkgFromTos = from+" to "+justPkgName;
}
根据拆分出来的任务,使用不同的线程进行构建。每次sjavac.compile调用都会创建新的com.sun.tools.javac.main.Main实例
for (int i=0; i<numCompiles; ++i) {
final int ii = i;
final CompileChunk cc = compileChunks[i];

requests[i] = new Thread() {
@Override
public void run() {
rn[ii] = sjavac.compile("n/a",
id + "-" + ii,
args.prepJavacArgs(),
Collections.<File>emptyList(),
cc.srcs,
visibleSources);
packageArtifacts.putAll(rn[ii].packageArtifacts);
packageDependencies.putAll(rn[ii].packageDependencies);
packagePublicApis.putAll(rn[ii].packagePublicApis);
classpathPackageDependencies.putAll(rn[ii].classpathPackageDependencies);
}
};
b.在sjavac9中,为了解决重复对同一个源文件反复隐式编译的问题,对并行编译任务之间的数据共享做了优化。
 
2.增量编译
不同于maven只编译变化的文件和按照模块为单位进行增量编译。sjavac实现了按照文件粒度的增量编译。其将每个文件和依赖的public api即公有方法,公有属性转化成一个hashcode,用来比较文件是否修改了pubapi。如果没有修改pubapi,则编译不需要传递,可以仅编译变化的部分。否则按照依赖关系进行传递,最终得到需要重新编译的文件集合。
 
总体来看,sjavac并不是一个实用的工具,但是其中的一些思想可能已经被gradle,bazel等编译工具借鉴。也是值得借鉴的。
 
 
posted @ 2019-02-24 22:31  lost陆离  阅读(745)  评论(0编辑  收藏  举报