Java HotSpot VM中的JIT编译

原文地址译者:郭蕾 校对:丁一

本文是Java HotSpot VM and just-in-time(JIT) compilation系列的第一篇。

Java HotSpot虚拟机是Oracle收购Sun时获得的,JVM和开源的OpenJDK都是以此虚拟机为基础发展的。如同其它虚拟机,HotSpot虚拟机为字节码提供了一个运行时环境。实际上,它主要会做这三件事情:

  • 执行方法所请求的指令和运算。
  • 定位、加载和验证新的类型(即类加载)。
  • 管理应用内存。

最后两点都是各自领域的大话题,所以这篇文章中只关注代码执行。

JIT编译

Java HotSpot是一个混合模式的虚拟机,也就是说它既可以解释字节码,又可以将代码编译为本地机器码以更快的执行。通过配置-XX:+PrintCompilation参数,你可以在log文件中看到方法被JIT编译时的信息。JIT编译发生在运行时 —— 方法经过多次运行之后。到方法需要使用到的时候,HotSpot VM会决定如何优化这些代码。

如果你好奇JIT编译带来的性能提升,可以使用-Djava.compiler=none将其关掉然后运行基准测试程序来看看它们的差别。

Java HotSpot虚拟机可以运行在两种模式下:client或者server。你可以在JVM启动时通过配置-client或者-server选项来选择其中一种。两种模式都有各自的适用场景,本文中,我们只会涉及到server模式。

两种模式最主要的区别是server模式下会进行更激进的优化 —— 这些优化是建立在一些并不永远为真的假设之上。一个简单的保护条件(guard condition)会验证这些假设是否成立,以确保优化总是正确的。如果假设不成立,Java HotSpot虚拟机将会撤销所做的优化并退回到解释模式。也就是说Java HotSpot虚拟机总是会先检查优化是否仍然有效,不会因为假设不再成立而表现出错误的行为。

在server模式下,Java HotSpot虚拟机会默认在解释模式下运行方法10000次才会触发JIT编译。可以通过虚拟机参数-XX:CompileThreshold来调整这个值。比如-XX:CompileThreshold=5000会让触发JIT编译的方法运行次数减少一半。(译者注:有关JIT触发条件可参考《深入理解Java虚拟机》第十一章以及《Java Performance》第三章HotSpot VM JIT Compilers小节)

这可能会诱使新手将编译阈值调整到一个非常低的值。但要抵挡住这个诱惑,因为这样可能会降低虚拟机性能,优化后减少的方法执行时间还不足以抵消花在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:

01 public class Main {
02     private static double timeTestRun(String desc, int runs,
03         Callable<Double> callable) throws Exception {
04         long start = System.nanoTime();
05         callable.call();
06         long time = System.nanoTime() - start;
07         return (double) time / runs;
08     }
09  
10     // Housekeeping method to provide nice uptime values for us
11     private static long uptime() {
12         return ManagementFactory.getRuntimeMXBean().getUptime() + 15;
13     // fudge factor
14     }
15  
16     public static void main(String... args) throws Exception {
17         int iterations = 0;
18         for (int i : new int[]
19             { 100, 1000, 5000, 9000, 10000, 11000, 13000, 20000, 100000} ) {
20             final int runs = i - iterations;
21             iterations += runs;
22  
23             // NOTE: We return double (sum of values) from our test cases to
24             // prevent aggressive JIT compilation from eliminating the loop in
25             // unrealistic ways
26             Callable<Double> directCall = new DFACaller(runs);
27             Callable<Double> viaGetSet = new GetSetCaller(runs);
28  
29             double time1 = timeTestRun("public fields", runs, directCall);
30             double time2 = timeTestRun("getter/setter fields", runs, viaGetSet);
31  
32             System.out.printf("%7d %,7d\t\tfield access=%.1f ns, getter/setter=%.1f ns%n",
33                 uptime(), iterations, time1, time2);
34             // added to improve readability of the output
35             Thread.sleep(100);
36         }
37     }
38 }

Listing1A:

01 public class DFACaller implements Callable<Double>{
02     private final int runs;
03  
04     public DFACaller(int runs_) {
05         runs = runs_;
06     }
07  
08     @Override
09     public Double call() {
10         DirectFieldAccess direct = new DirectFieldAccess();
11         double sum = 0;
12         for (int i = 0; i < runs; i++) {
13             direct.one++;
14             sum += direct.one;
15         }
16         return sum;
17     }
18 }
19  
20 public class DirectFieldAccess {
21     int one;
22 }

Listing1B:

01 public class GetSetCaller implements Callable<Double> {
02     private final int runs;
03  
04     public GetSetCaller(int runs_) {
05         runs = runs_;
06     }
07  
08     @Override
09     public Double call() {
10         ViaGetSet getSet = new ViaGetSet();
11         double sum = 0;
12         for (int i = 0; i < runs; i++) {
13             getSet.setOne(getSet.getOne() + 1);
14             sum += getSet.getOne();
15         }
16         return sum;
17     }
18 }
19  
20 public class ViaGetSet {
21     private int one;
22  
23     public int getOne() {
24         return one;
25     }
26  
27     public void setOne(int one) {
28         this.one = one;
29     }
30 }

如果使用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都是比较慢的,这是因为第一次运行时包含了类加载的时间,下一行就比较快了,尽管此时还没有任何代码被编译。

另外要注意下面几点:

  • 在遍历1000和5000次时,直接操作字段比使用getter/setter方法快,因为getter 和setter还没有内联或优化。即便如此,它们都还相当地快。
  • 在遍历9000次时,getter方法被优化了(因为每次循环中调用了两次),使性能有小许提高。
  • 在遍历10000次时,setter方法也被优化了,因为需要额外花费时间去优化,所以执行速度降下来了。
  • 最终,两个测试类都被优化了:
    • DFACaller直接操作字段,GetSetCaller使用getter和setter方法。此时它们不仅刚被优化,还被内联了。
    • 从下一次的遍历中可以看到,测试用例的执行时间仍不是最快的。
  • 在13000次遍历之后,两种字段访问方式的性能都和最后更长时间测试的结果一样好,我们已经达到了性能的稳定状态。

需要特别注意的是,直接访问字段和通过getter/setter访问在稳定状态下的性能是基本一致的,因为方法已经被内联到GetSetCaller中,也就是说在viaGetSet中所做的事情和directCall中完全一样。

JIT编译是在后台进行的。每次可用的优化手段可能随机器的不同而不同,甚至,同个程序的多次运行期间也可能不一样。

总结

这篇文章中,我所描述的只是JIT编译的冰山一角,尤其是没有提到如何写出好的基准测试以及如何使用统计信息以确保不会被平台的动态性所愚弄。

这里使用的基准测试非常简单,不适合做为真实的基准测试。在第二部分,我计划向您展示一个真实的基准测试并继续深入JIT编译的过程。 

时间: 2024-11-01 21:19:13

Java HotSpot VM中的JIT编译的相关文章

你的Java代码对JIT编译友好么?(转)

JIT编译器是Java虚拟机(以下简称JVM)中效率最高并且最重要的组成部分之一.但是很多的程序并没有充分利用JIT的高性能优化能力,很多开发者甚至也并不清楚他们的程序有效利用JIT的程度. 在本文中,我们将介绍一些简单的方法来验证你的程序是否对JIT友好.这里我们并不打算覆盖诸如JIT编译器工作原理这些细节.只是提供一些简单基础的检测和方法来帮助你的代码对JIT友好,进而得到优化. JIT编译的关键一点就是JVM会自动地监控正在被解释器执行的方法.一旦某个方法被视为频繁调用,这个方法就会被标记

你的 Java 代码对 JIT 编译友好么?

JIT编译器是Java虚拟机(以下简称JVM)中效率最高并且最重要的组成部分之一.但是很多的程序并没有充分利用JIT的高性能优化能力,很多开发者甚至也并不清楚他们的程序有效利用JIT的程度. 在本文中,我们将介绍一些简单的方法来验证你的程序是否对JIT友好.这里我们并不打算覆盖诸如JIT编译器工作原理这些细节.只是提供一些简单基础的检测和方法来帮助你的代码对JIT友好,进而得到优化. JIT编译的关键一点就是JVM会自动地监控正在被解释器执行的方法.一旦某个方法被视为频繁调用,这个方法就会被标记

JVM深入学习笔记二:Java JIT编译

JIT是java虚拟机把热点字节码编译成机器码的技术. 解释执行,在当运行次数比较少的时候能够省去编译的操作直接运行字节码.  另外解释更加的节约内存. 而编译为机器码则可以获得更高的效率. 因为各有好处,HotSpot使用了共存的机制,可以使用-Xint强制使用解释模式或者是-Xcomp 编译模式. 此外HotSpot提供了两种编译器Client Compile和Server Compiler,分别针对于更快的编译速度和更好的编译效果.使用-client或者-server参数指定 在这种共存模

java中执行gcc编译,得不到编译结果

问题描述 java中执行gcc编译,得不到编译结果 Process process = Runtime.getRuntime().exec(cmd); BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream())); String s = br.readline(); 可以得到一般命令的输出,但是GCC编译出错时结果得不到 解决方案 后来我尝试用gcc a.cpp -o a.cpp 2

聊聊并发(二)Java SE1.6中的Synchronized

本文属作者原创,原文发表于InfoQ:http://www.infoq.com/cn/articles/java-se-16-synchronized 1 引言 在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些情况下它并不那么重了,本文详细介绍了Java SE1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程. 2 术语定义 术语 英

X86 DBCA, NETCA GIVE JAVA HOTSPOT ERROR IF ON X86_64 HARDWARE

    在使用DBCA命令创建新的数据库时,DBCA命令无法启动.运行的环境是宿主机64bit+AMD cpu, 而客户机为Linux 32bit + Grid Infrastructure(32) + Oracle database software(32)的情形.原本想着32bit运行的会快一点,没想到Bug 8670579 在执行dbca时再一次被触发,根据Oracel描述,类似的NETCA也会触发这个Bug.  一.故障现象    [oracle@linux1 ~]$ dbca    #

Java 编程技术中汉字问题的分析及解决(转)

编程|汉字|解决|问题 Java 编程技术中汉字问题的分析及解决 段明辉自由撰稿人2000 年 11月 8日内容: 汉字编码的常识 Java 中文问题的初步认识 Java 中文问题的表层分析及处理 Java 中文问题的根源分析及解决 Java Servlet 中文问题的根源 修改 Servlet.jar 中文乱码的处理函数 参考资料 作者简介在基于 Java 语言的编程中,我们经常碰到汉字的处理及显示的问题.一大堆看不懂的乱码肯定不是我们愿意看到的显示效果,怎样才能够让那些汉字正确显示呢?Jav

Java 编程技术中汉字问题的分析及解决,文件操作

编程|汉字|解决|问题 在基于 Java 语言的编程中,我们经常碰到汉字的处理及显示的问题.一大堆看不懂的 乱码肯定不是我们愿意看到的显示效果,怎样才能够让那些汉字正确显示呢?Java 语言 默认的编码方式是UNICODE ,而我们中国人通常使用的文件和数据库都是基于 GB2312 或者 BIG5 等方式编码的,怎样才能够恰当地选择汉字编码方式并正确地处理汉字的编 码呢?本文将从汉字编码的常识入手,结合 Java 编程实例,分析以上两个问题并提出 解决它们的方案. 现在 Java 编程语言已经广

在JAVA应用程序中如何实现FTP的功能 (转)

程序 在JAVA应用程序中如何实现FTP的功能 大连捷通电脑技术有限公司 王 淼 ---- 在JAVA的编程中,您也许会遇到FTP方面的编程,本文就来演示如何实现它. ---- 本程序是由JBUILDER2.0来开发的,为了节约篇幅我只列出主要的三个部份.FtpList 部分是用来显示FTP服务器上的文件(附图略).GetButton部分为从FTP服务器下传一个文件.PutButton 部分为向FTP服务器上传一个文件.别忘了在程序中还要引入两个库文件(import sun.net.*,impo