Java HotSpot虚拟机是Oracle收购Sun时得到的,JVM和开源的OpenJDK都是以此虚拟机为基础发展的。如同其它虚拟机,HotSpot虚拟机为字节码提供了一个运行时环境。实际上,它主要会作这三件事情:java
最后两点都是各自领域的大话题,因此这篇文章中只关注代码执行。git
Java HotSpot是一个混合模式的虚拟机,也就是说它既能够解释字节码,又能够将代码编译为本地机器码以更快的执行。经过配置-XX:+PrintCompilation
参数,你能够在log文件中看到方法被JIT编译时的信息。JIT编译发生在运行时 —— 方法通过屡次运行以后。到方法须要使用到的时候,HotSpot VM会决定如何优化这些代码。segmentfault
若是你好奇JIT编译带来的性能提高,可使用-Djava.compiler=none
将其关掉而后运行基准测试程序来看看它们的差异。网络
Java HotSpot虚拟机能够运行在两种模式下:client或者server。你能够在JVM启动时经过配置-client
或者-server
选项来选择其中一种。两种模式都有各自的适用场景,本文中,咱们只会涉及到server模式。oracle
两种模式最主要的区别是server模式下会进行更激进的优化 —— 这些优化是创建在一些并不永远为真的假设之上。一个简单的保护条件(guard condition)会验证这些假设是否成立,以确保优化老是正确的。若是假设不成立,Java HotSpot虚拟机将会撤销所作的优化并退回到解释模式。也就是说Java HotSpot虚拟机老是会先检查优化是否仍然有效,不会由于假设再也不成立而表现出错误的行为。ide
在server模式下,Java
HotSpot虚拟机会默认在解释模式下运行方法10000次才会触发JIT编译。能够经过虚拟机参数-XX:CompileThreshold
来调整这个值。好比-XX:CompileThreshold=5000
会让触发JIT编译的方法运行次数减小一半。(译者注:有关JIT触发条件可参考《深刻理解Java虚拟机》第十一章以及《Java Performance》第三章HotSpot VM JIT Compilers小节)oop
这可能会诱使新手将编译阈值调整到一个很是低的值。但要抵挡住这个诱惑,由于这样可能会下降虚拟机性能,优化后减小的方法执行时间还不足以抵消花在JIT编译上的时间。性能
当Java HotSpot虚拟机能为JIT编译收集到足够多的统计信息时,性能会最好。当你下降编译阈值时,Java HotSpot虚拟机可能会在非热点代码的编译中花费较多时间。有些优化只有在收集到足够多的统计信息时才会进行,因此下降编译阈值可能致使优化效果不佳。测试
另一方面,不少开发者想让一些重要方法在编译模式下尽快得到更好的性能。优化
解决此问题通常是在进程启动后,对代码进行预热以使它们被强制编译。对于像订单系统或者交易系统来讲,重要的是要确保预热不会产生真实的订单。
Java HotSpot虚拟机提供了不少参数来输出JIT的编译信息。最经常使用的就是前文提到的PrintCompilation,也还有一些其它参数。
接下来咱们将使用PrintCompilation来观察Java HotSpot虚拟机在运行时编译方法的成效。但先有必要说一下用于计时的System.nanoTime()方法。
Java为咱们提供了两个主要的获取时间值的方法:currentTimeMillis()和nanoTime().前者对应于咱们在实体世界中看到的时间(所谓的钟表时间),它的精度能知足大多数状况,但不适用于低延迟的应用。
纳秒计时器拥有更高的精度。这种计时器度量时间的间隔极短。1纳秒是光在光纤中移动20CM所需的时间,相比之下,光经过光纤从伦敦传送到纽约大约须要27.5毫秒。
由于纳秒级的时间戳精度过高,使用不当就会产生较大偏差,所以使用时须要注意。
如,currentTimeMillis()能很好的在机器间同步,能够用于测量网络延迟,但nanoTime()不能跨机器使用。
接下来将上面的理论付诸实践,来看一个很简单(但极其强大)的JIT编译技术。
方法内联是编译器优化的关键手段之一。方法内联就是把方法的代码“复制”到发起调用的方法里,以消除方法调用。这个功能至关重要,由于调用一个小方法可能比执行该小方法的方法体耗时还多。
JIT编译器能够进行渐进内联,开始时内联简单的方法,若是能够进行其它优化时,就接着优化内联后的较大的代码块。
Listing1,Listing1A以及Listing1B是个简单的测试,将直接操做字段和经过getter/setter方法作了对比。若是简单的getters和setters方法没有使用内联的话,那调用它们的代价是至关大的,由于方法调用比直接操做字段代价更高。
Listing1:
public class Main { private static double timeTestRun(String desc, int runs, Callable<Double> callable) throws Exception { long start = System.nanoTime(); callable.call(); long time = System.nanoTime() - start; return (double) time / runs; } // Housekeeping method to provide nice uptime values for us private static long uptime() { return ManagementFactory.getRuntimeMXBean().getUptime() + 15; // fudge factor } public static void main(String... args) throws Exception { int iterations = 0; for (int i : new int[] { 100, 1000, 5000, 9000, 10000, 11000, 13000, 20000, 100000} ) { final int runs = i - iterations; iterations += runs; // NOTE: We return double (sum of values) from our test cases to // prevent aggressive JIT compilation from eliminating the loop in // unrealistic ways Callable<Double> directCall = new DFACaller(runs); Callable<Double> viaGetSet = new GetSetCaller(runs); double time1 = timeTestRun("public fields", runs, directCall); double time2 = timeTestRun("getter/setter fields", runs, viaGetSet); System.out.printf("%7d %,7d\t\tfield access=%.1f ns, getter/setter=%.1f ns%n", uptime(), iterations, time1, time2); // added to improve readability of the output Thread.sleep(100); } } }
Listing1A:
public class DFACaller implements Callable<Double>{ private final int runs; public DFACaller(int runs_) { runs = runs_; } @Override public Double call() { DirectFieldAccess direct = new DirectFieldAccess(); double sum = 0; for (int i = 0; i < runs; i++) { direct.one++; sum += direct.one; } return sum; } } public class DirectFieldAccess { int one; }
Listing1B:
public class GetSetCaller implements Callable<Double> { private final int runs; public GetSetCaller(int runs_) { runs = runs_; } @Override public Double call() { ViaGetSet getSet = new ViaGetSet(); double sum = 0; for (int i = 0; i < runs; i++) { getSet.setOne(getSet.getOne() + 1); sum += getSet.getOne(); } return sum; } } public class ViaGetSet { private int one; public int getOne() { return one; } public void setOne(int one) { this.one = one; } }
若是使用java -cp. -XX:PrintCompilation Main
运行测试用例,就能看到性能上的差别(见Listing2)。
Listing2
31 1 java.lang.String::hashCode (67 bytes) 36 100 field access=1970.0 ns, getter/setter=1790.0 ns 39 2 sun.nio.cs.UTF_8$Encoder::encode (361 bytes) 42 3 java.lang.String::indexOf (87 bytes) 141 1,000 field access=16.7 ns, getter/setter=67.8 ns 245 5,000 field access=16.8 ns, getter/setter=72.8 ns 245 4 ViaGetSet::getOne (5 bytes) 348 9,000 field access=16.0 ns, getter/setter=65.3 ns 450 5 ViaGetSet::setOne (6 bytes) 450 10,000 field access=16.0 ns, getter/setter=199.0 ns 553 6 Main$1::call (51 bytes) 554 7 Main$2::call (51 bytes) 556 8 java.lang.String::charAt (33 bytes) 556 11,000 field access=1263.0 ns, getter/setter=1253.0 ns 658 13,000 field access=5.5 ns, getter/setter=1.5 ns 760 20,000 field access=0.7 ns, getter/setter=0.7 ns 862 100,000 field access=0.7 ns, getter/setter=0.7 ns
这些是什么意思?Listing2中的第一列是程序启动到语句执行时所通过的毫秒数,第二列是方法ID(编译后的方法)或遍历次数。
注意:测试中没有直接使用String和UTF_8类,但它们仍然出如今编译的输出中,这是由于平台使用了它们。
从Listing2中的第二行能够发现,直接访问字段和经过getter/setter都是比较慢的,这是由于第一次运行时包含了类加载的时间,下一行就比较快了,尽管此时尚未任何代码被编译。
另外要注意下面几点:
最终,两个测试类都被优化了:
在13000次遍历以后,两种字段访问方式的性能都和最后更长时间测试的结果同样好,咱们已经达到了性能的稳定状态。
须要特别注意的是,直接访问字段和经过getter/setter访问在稳定状态下的性能是基本一致的,由于方法已经被内联到GetSetCaller中,也就是说在viaGetSet中所作的事情和directCall中彻底同样。
JIT编译是在后台进行的。每次可用的优化手段可能随机器的不一样而不一样,甚至,同个程序的屡次运行期间也可能不同。
这篇文章中,我所描述的只是JIT编译的冰山一角,尤为是没有提到如何写出好的基准测试以及如何使用统计信息以确保不会被平台的动态性所愚弄。
这里使用的基准测试很是简单,不适合作为真实的基准测试。在第二部分,我计划向您展现一个真实的基准测试并继续深刻JIT编译的过程。
原文 Introduction to JIT Compliation in Java Hotspot VM
译者 郭蕾
校对丁一
via ifeve