HotSpot VM JIT的编译产出,理论上能否被复用?

使用 JVM 的大规模的分布式计算中(如 Hadoop MapReduce),对于同质化(代码相同,外部条件、输入数据相似)的代码,仍然需要在不同进程、不同节点上独自进行 JIT 编译。 对编译产出的复用,在理论上是否可行?
关注者
47
被浏览
1213
题主原本的问题:
HotSpot VM JIT的编译产出,理论上能否被复用?
使用 JVM 的大规模的分布式计算中(如 Hadoop MapReduce),对于同质化(代码相同,外部条件、输入数据相似)的代码,仍然需要在不同进程、不同节点上独自进行 JIT 编译。
对编译产出的复用,在理论上是否可行?
问题说“理论上是否可行”,答案是肯定的。但要追问现在的HotSpot VM是否已经有这样的功能,那么可惜的是,到Oracle JDK 8 / OpenJDK 8为止,还没有。

题主所想像的功能有两种可能的实现方式:
  • AOT:Ahead-of-Time Compilation。与传统的C或C++的编译器相似,在程序运行之前就完成到机器码的编译。
  • Dynamic AOT / JIT Caching:把一次运行中JIT编译的结果保存下来,下次运行(或者例如说同一台机器上别的Java进程运行)就把它加载出来。

题主提问的角度是从dynamic AOT的方向出发的。现实中确实也有产品级JVM已经提供这样的功能了——例如IBM J9 VM。J9从IBM JDK6开始就伴随Class Data Sharing功能提供jitdata caching,做法正是题主所想像的dynamic AOT。请参考 Enhance performance with class sharing
但——这里千万要注意了:J9的dynamic AOT模式下JIT编译出来的代码,跟正常模式下JIT编译出来的代码是不一样的,前者比后者慢。引用上面链接的IBM文档所说:
AOT code is native code and generally executes faster than interpreted code (although it is not likely to run as fast as JIT-generated code).

普通AOT方面其实Java世界已经有很多实现了。Excelsior JET、GCJ之类都是经验丰富的老兵了。
而最新加入战团的是Oracle JDK 9——Oracle计划在JDK9的某个更新版发布带有静态AOT功能的编译器,这个编译器基于Graal。请参考 Java Goes AOT - JVMLS 2015 。就它目前的实现看,它AOT编译出来的代码还是比HotSpot正常JIT编译出来的代码要慢。

IBM J9的dynamic AOT生成的代码,与Oracle JDK 9的更新版将要发布的AOT生成的代码,默认都是可以进一步触发JIT编译的,这样就结合了AOT代码快速启动与JIT代码速度快两者的优势,强强联手。
Oracle JDK 9的更新版的那个AOT也可以配置为不进一步触发JIT编译。这种模式可以用于目标环境不允许JIT编译的情况,例如iOS上。但这样会降低程序的最高性能。

上面说的信息我已经在知乎回答过很多次,请参考汇总传送门:
Java中有类似于NGen的工具(AOT编译器)吗? - RednaxelaFX 的回答

=========================================

为啥在JVM上AOT编译普遍比JIT编译生成的代码质量还要差呢?这是因为现代主流高性能JVM的JIT编译器都会做非常激进的乐观优化(aggressive speculative optimization),这会利用上各种运行时能获取的信息,例如:
  • 当前已加载的类的类层次结构(class hierarchy)
  • 方法A调用方法B,如果方法B已经被JIT编译了并且A决定不内联B,则在JIT编译方法A时会把方法B的被编译后的机器码的入口地址嵌入到A里的调用点上。
  • 同上,假如JIT编译后的代码涉及访问一些JVM内部数据结构的地址(例如某个类的元数据对象的地址),则这些地址都会直接被嵌入到JIT编译后的代码中。

一般的Java代码风格下,每个方法普遍偏小,要想尽量优化就得尽可能的(并且合理的)做方法内联。这是JIT比AOT要容易做得多的地方,而前者比后者代码快的主要源头也就来自于此。

要做AOT编译,无论是普通AOT还是dynamic AOT,都不能在编译后的代码里这么随意的嵌入地址。本质上来说要生成position-independent code(PIC)。
  • 普通AOT:各种“地址”根本就还不存在,也就只能生成PIC了。
  • Dynamic AOT:虽然是把一次运行中JIT编译的代码给缓存下来,但如何保证缓存下来的代码下次还会被加载到完全一样的地址,而它所依赖的嵌入进来的地址也都还在完全一样的地址呢?
    • 解决办法1:生成PIC(代码带有PLT/GOT的对应物),然后在下次加载的时候代码部分的内存还是可以共享,而PLT/GOT的对应部分则运行时填值,不共享内存;
    • 解决办法2:跟JIT相似生成内嵌地址的代码,但同时生成记录哪里嵌了地址的relocation info;下次加载时会根据当时运行时的地址把代码里内嵌的地址都patch到正确的地址上。这样patch过的代码所占用的内存就无法与其它进程共享了;
    • 解决办法3:简单粗暴型——如果下次加载时无法映射到同样的地址则放弃加载。这里说的“地址”不但包括代码的地址,还有它所嵌入的所有其它东西的地址。这种可以共享所有内存,但是很可能会加载失败,特别是在地址空间受限的环境中(例如32位系统上)。

我不太确定IBM J9的dynamic AOT具体生成的代码是哪种情况,但无论如何它为了避免频繁的加载失败都肯定会降低其(相对于正常JIT模式)的优化程度。

其实Sun在JDK6的开发过程中,也有同事尝试过做一个基于HotSpot C1的dynamic AOT的原型,结果发现那个原型在缩短Java应用的启动时间上相比多层编译(tiered compilation)并没有显著提升,于是那个原型就被放弃了。

=========================================

其实要改善Java应用的启动性能,除了AOT编译之外还有很多别的事情可以做。Azul Systems的Zing JVM所提供的ReadyNow!功能就是我们在这方面上的尝试。有兴趣的同学可以关注一下:Zing
ReadyNow!的简介在此:Zing® ReadyNow! : ™ : A ‘No Stalls’ Java that starts up fast and stays fast