JIT Compiler(即时编译器)全称为Just In Time,为JVM的一部分。众所周知,JIT技术可以提高Java代码的运行速度。本文将将通过以下问题来了解分析JIT:
- 什么是JIT?
- JIT如何有针对性地优化代码
- 如何优化JIT
1. 什么是JIT?
在解释JIT是什么之前,让我们一起了解以下Java代码从源码到被机器执行的流程。
Java在被JVM执行前需要被编译,即将我们写在.java
文件中的源代码转换成.class
文件中的字节码。通常在IntelliJ、Eclipse等IDE中已经集成了这类编译器,所以无需我们手动进行编译。如果要手动编译Java源码,需要用javac
命令进行编译,例如javac Test.java。 这里的编译为运行前编译,代码完成运行前编译后就可以直接交付JVM运行了。Java的字节码是平台无关的,因为JVM会将相对应的字节码解释为可执行的汇编码。解释在JVM启动时开始,是字节码执行最慢的形式。这里Java就相当于和Python一样的解释性语言,当然Java并非解释性语言,而是“混合模式”语言,同时运用解释执行和编译执行两种方式。Java如何将字节码一一对应到机器码则不在本文的讨论范围内。
为什么解释执行慢?因为JVM每次运行相应的字节码时,都要将字节码解释成对应的机器码。其中有很多的重复性工作,例如一个将被运行10000次的循环将被解释10000次。为了提高运行效率,JIT顺势登场。
JIT是JVM中的一个自适应优化器,会有针对性地优化被JVM证明为代码性能关键的方法。JIT的优化则是将该方法的代码编译保存,随后JVM运行该方法时就无需再对方法进行解释转换为汇编码。在了解了什么是JIT后,下面我们一起看看JIT是如何工作的。
2. JIT如何有针对性地优化代码
既然JIT会对代码中性能关键的方法进行优化,那么JIT是如何识别这些方法的呢?为了确定代码中的性能关键方法,JVM会在代码解释运行时监控关注以下指标:
- 方法进入计数器:为每个方法分配一个调用计数器。
- 循环分支计数器:为每个已执行的循环分配一个计数器。
如果一个方法的调用次数超过JVM设定的编译临界值,则该方法被认为是代码中的性能关键方法;同理,假如一个循环执行次数超过JVM设定的编译临界值,则该循环也被认为是性能关键的。如第一部分指出,JIT对这些性能关键方法的优化就是将代码编译保存,并在下一次调用时执行。这里需要注意的是:当一个循环被编译后,再进入循环执行的就是编译过后的代码,而非将字节码翻译成机器码。这里的优化被称作栈上替换(OSR: On Stack Replacement),因为JVM是在栈上替换编译代码的。
在了解了JIT如何确定性能关键方法后,我们该如何根据JIT的特性对它进行相应的优化,使之更好地适应我们的代码呢?
3. 如何优化JIT
3.1 选择编译临界值
从第二部分我们可以知道,JIT有编译的临界值。编译临界值是可以选择的,比如最简单的就是选择不同的JVM模式。Java为JIT提供了两种编译模式,分别是client
和server
模式。二者的区别是编译的临界值不同:
- Client模式下,JIT的编译临界值比较低,为1500。
- Server模式下,JIT的编译临界值比较高,为10000。
根据不同应用的需要,我们可以选择合适的模式。我们可以通过java --version
命令行来找到我们用的是什么模式,通常JDK提供的是混合模式。以防需要,这里有一个修改模式的方法,仅供参考:
StackOverflow: Change default java vm to client
注意:我按照这个方式尝试了,但是没有成功。jvm.cfg文件对用户只有可读权限,我用
sudo chmod
命令修改权限并修改了模式,但是未能改变编译模式。
3.2 JIT的分层编译
我们常用的OpenJDK HotSpot VM就使用了分层编译。我们可以通过-XX:+TieredCompilation
命令行来启用分成编译。从InfoQ的一篇文章中找到了一段关于分层编译的描述:
With the introduction of tiered compilation, OpenJDK HotSpot VM users can benefit from improved startup times with the server compiler.
Tiered compilation has five tiers of optimization. It starts in tier-0, the interpreter tier, where instrumentation provides information on the performance critical methods. Soon enough the tier 1 level, the simple C1 (client) compiler, optimizes the code. At tier 1, there is no profiling information. Next comes tier 2, where only a few methods are compiled (again by the client compiler). At tier 2, for those few methods, profiling information is gathered for entry-counters and loop-back branches. Tier 3 would then see all the methods getting compiled by the client compiler with full profiling information, and finally tier 4 would avail itself of C2, the server compiler.
3.3 分层编译与代码缓存
分层编译带来的另一个好处是我们可以针对每一层进行编译临界值的设定,例如
-XX:Tier3MinInvocationThreshold,
-XX:Tier3CompileThreshold,
-XX:Tier3BackEdgeThreshold
第三层编译的最低阈值为100,这样,大量的方法都将被编译。大量方法被编译将产生新的问题。因为JVM会将编译后的代码存放在代码缓存中,编译越多的代码就需要越多的缓存空间。在OpenJDK中默认可用的分层编译代码缓存大小为240MB,而用于不分层的代码缓存大小只有48MB。JVM会在代码缓存满时发出警告,我们也可以通过-XX:ReservedCodeCacheSize来设定我们需要的代码缓存的大小。
3.4 获取编译信息
为了更好地了解编译情况,OpenJDK HotSpot VM提供了-XX:+PrintCompilation
命令来告诉用户代码的编译情况。