麻烦你们帮我投一票哈,谢谢前端
常常听到 Java 性能不如 C/C++ 的言论,也常常据说 Java 程序须要预热,那么其中主要缘由是啥呢?java
面试的时候谈到 JVM,也有不少面试官喜欢问,为啥 Java 程序越执行越快呢?面试
通常人都能回答上来,类加载,缓存预热等等,可是深刻下去,最重要的却没有答上来,今天本系列文章就来帮助你们理解这个问题的关键。首先是 JIT 优化算法
首先,咱们从一个简单的例子看起,来感觉下程序是否愈来愈快:后端
package com.test; import java.util.concurrent.TimeUnit; public class CompileTest { public static void main(String[] args) throws InterruptedException { while (true) { test1(); TimeUnit.SECONDS.sleep(1); } } public static void test1() { long time1 = System.nanoTime(); long count1 = 0; for (int i = 0; i < 10000; i++) { count1++; } //为了和编译日志区分,这里输出到error输出 System.err.println(System.nanoTime() - time1 + "-----" + count1); } }
运行时,加上参数-XX:+PrintCompilation
,打印一下编译日志(其实这个参数之后也许就过时了,建议使用 JVM 标准日志参数:-Xlog:jit+compilation=info
),能够看到:缓存
432900-----10000 250800-----10000 194600-----10000 197200-----10000 131600-----10000 184000-----10000 6369 374 % 3 com.test.CompileTest::test1 @ 9 (61 bytes) 162300-----10000 7369 375 3 com.test.CompileTest::test1 (61 bytes) 68300-----10000 60300-----10000 47200-----10000 48100-----10000 11371 378 % 4 com.test.CompileTest::test1 @ 9 (61 bytes) 55600-----10000 11388 374 % 3 com.test.CompileTest::test1 @ 9 (61 bytes) made not entrant 12372 379 4 com.test.CompileTest::test1 (61 bytes) 157600-----10000 12389 375 3 com.test.CompileTest::test1 (61 bytes) made not entrant 600-----10000 700-----10000 600-----10000 1200-----10000 900-----10000 900-----10000
从输出中能够看出,貌似JVM对test1这段代码作了一些事情,使方法运行愈来愈快了。这就是JIT作的优化,随着代码的执行,热点代码会被优化,让执行更加迅速。这也是为何,经过通常方法(javac命令)编译出来java class文件在执行的时候,要预热以后,才能发挥最大性能。接下来,咱们来详细介绍下JIT。服务器
OpenJDK Hotspot JVM,是最普遍运用的Java JVM。主要包含两部分,执行引擎(execution engine)和运行时(runtime)。执行引擎包括两部分,一个是垃圾收集器,另外一个就是咱们今天的主题, JIT(just-in-time)编译器。ide
JVM是Java一次编译,跨平台执行的基础。当java被编译为字节码形式的class文件以后,他能够在任意的JVM运行。这里说的编译,主要是指前端编译器。oop
Java中主要有两种编译器:性能
前端编译器,将.java文件编译为JVM可执行的.class字节码文件,即javac,主要职责包括:词法、语法分析,填充符号表,语义分析,字节码生成。输出为字节码文件,也能够理解为是中间表达形式(称为IR:Intermediate Representation)。对应上面的例子就是将CompileTest.java
编译成符合Java规范的字节码文件CompileTest.class
Java 9中还引入了实验编译器AOT(Ahead-Of-Time)编译器,直接生成机器码。主要用于减小JAVA启动预热时间,比较适用于单次执行时间有限须要高效执行的程序,或者是小集成芯片环境,对效率要求比较高。AOT与Graal咱们会在系列的最后着重介绍。对应上面的例子就是,test1方法不用预热就会执行的和上面最会同样那么快。可是相应的,机器码占用的大小比字节码大的多得多,并且不能跨平台。
为何要这么区分呢?首先,不一样机器的机器码是不同的,编译生成统一的字节码保证了跨平台应用的可能性。而后,将字节码优化(中间表达形式优化)放到运行时优化,这样低版本的java编译出来的字节码,在高版本的JVM运行,仍能享受高版本的JVM新的优化机制带来的性能提高,这是一种很好的向后兼容机制。因此有的时候,咱们能够先把JVM升级到新版原本享受更高效的优化算法。
刚刚提到了JVM使用混合模式来从字节码转换成机器能够运行的机器码,混合模式包括解释器和JIT:
解释器工做机制:
在编译时,主要是将java源代码文件编译为java统一的字节码,可是编译成的字节码并不能直接运行,而是经过JVM读取运行。JVM中的解释器就是将.class文件一行一行翻译以后再运行,翻译就是转换成当前机器能够运行的机器码,它不会一次性把整个文件都翻译过来,而是翻译一句,执行一句,再翻译,再执行,因此解释器的程序运行起来会比较慢,每次都要解释以后再执行。因此,有些时候,咱们想是否能够把解释以后的内容缓存起来,这样不就能够直接运行了?可是,若是每段代码都要缓存起来,例如仅仅执行一次的代码也缓存起来,这样太浪费内存了。因此,引入一个新的运行时编译器,JIT来解决这些问题,加速热点代码的执行。
JIT运行时编译器工做机制:
JIT针对热点代码,进行编译与深度优化,优化后的机器码会被缓存起来,存入CodeCache中。对于非热点代码,例如只运行一次的代码(类构造器等等),直接解释执行,更加快速。JIT不只花更多时间去编译优化,并且还多耗费了不少内存,而且 CodeCache 发生变化会发生部分或者全部线程进入 Safepoint 致使 Stop the world。字节码转换为可执行的机器码,大小会大不少不少倍。这也是为啥,解释器每次都要翻译而且执行,JIT只针对热点代码进行编译优化的缘由。JIT编译器执行的一些常见优化操做包括数据分析,从堆栈操做到寄存器操做的转换,经过寄存器分配减小内存访问,消除常见子表达式等。JIT编译器进行的优化程度越高,在执行阶段花费的时间越多。所以,JIT编译器没法承担全部静态编译器所作的优化,这不只是由于增长了执行时间的开销,并且还由于它只对程序进行了限制。这也就解释了为何有些JVM会选择不老是作JIT编译,而是选择用解释器+JIT编译器的混合执行引擎。
对于上面的例子,刚开始的时候,test1方法是解释器执行的,因为多了一步转换,因此比较慢。后面随着代码的运行和JIT优化,test1方法的机器码被优化而且存入代码缓存,下次执行直接从代码缓存读取执行。
首先,须要判断一个方法是不是热点方法:在HotSpot虚拟机中使用的基于计数器的热点探测方法,他为每一个方法都准备了两个计数器:方法调用计数器和loop-back-edge计数器。
有了这些计数器,JIT能够根据这些计数器里面的统计信息,进行优化。固然,不止有这些计数器,还有一些其余更复杂的采集点。JIT编译器在JDK 8以前,例如JDK 7是区分client模式(C1编译器)仍是server模式(C2编译器)的,从JDK 8开始,不作这个区分了,都是C1+C2编译器合做,分层优化。C1是一个简单快速的编译器,主要关注点在于局部优化,而放弃许多耗时较长的全局优化手段。C2则是专门面向服务器端的,并为服务端的性能配置特别调整过的编译器,是一个充分优化过的高级编译器。从Java 8开始,JIT编译优化是分层优化,分为5层,每层都会有C1或者C2参与。
每日一刷,轻松提高技术,斩获各类offer: