在“什么是即时编译(JIT)!?OpenJDK HotSpot
VM剖析”这篇文章里,作者提到HotSpot执行引擎有一个即时(JIT)编译器。为了优化启动时间,分层编译先对代码进行解释,然后把它们快速移动到第1层,第2层和第3层,在这些层里使用客户端编译级别对它们进行编译(使用不同的剖析信息),最后把它们移动到服务端编译级别的层(更多信息可以参考上面的文章)。尽管有编译阶段的优化,HotSpot仍然会先解释执行字节码,然后才会使用即时编译。

转:什么是即时编译(JIT)!?OpenJDK HotSpot VM剖析,jitopenjdk

 以前有句话说:“Java是解释执行的 ”
。现在看来确实不是很准确,至于原因,在此简略解释:

今年9月,一个关于在HotSpot里添加预先编译(Ahead-of-Time,AOT)的提案被提交到JEP。AOT通过加载预编译的类来优化启动时间,避免了在解释模式或局部优化编译级别运行这些类。

重点

  • 应用程序可以选择一个适当的即时编译器来进行接近机器级的性能优化。
  • 分层编译由五层编译构成。
  • 分层编译提供了极好的启动性能,并指导编译的下一层编译器提供高性能优化。
  • 提供即时编译相关诊断信息的JVM开关。
  • 像内联化和向量化之类的优化进一步增强了性能。

OpenJDK HotSpot Java Virtual
Machine被人亲切地称为Java虚拟机或JVM,由两个主要组件构成:执行引擎和运行时。JVM和Java
API组成Java运行环境,也称为JRE。

澳门新葡萄京官网注册 1

在本文中,我们将探讨执行引擎,特别是即时编译,以及OpenJDK HotSpot
VM的运行时优化。

 

AOT并非新出现的动态编译器技术。IBM的J9虚拟机就支持AOT,Excelsior
JET和其它一些虚拟机也支持。AOT使用(共享)已经编译成本地代码的库让动态编译器达到更好的启动/预热效果。

JVM的执行引擎和运行时

执行引擎由两个主要组件构成:垃圾回收器(它回收垃圾对象并提供自动的内存或堆管理))以及即时编译器(它把字节码转换为可执行的机器码)。在OpenJDK
8中,“分层的编译器”是默认的服务端编译器。HotSpot也可以通过禁用分层的编译器(-XX:-TieredCompilation)仍然选择不分层的服务端编译器(也称为“C2”)。我们接下来将了解这些编译器的更多内容。

澳门新葡萄京官网注册 2

JVM的运行时掌控着类的加载、字节码的验证和其他以下列出的重要功能。其中一个功能是“解释”,我们将马上对其进行深入地探讨。你可以点击此处了解JVM运行时的更多内容。

相关厂商内容

 首先,我们先解释一下在Java中解释执行和编译执行的区别。 

跟JIT编译器类似,AOT编译也有分层和非分层两种模式,不同之处在于剖析信息和JIT再编译。那篇文章提到,在分层模式下,编译第2层会收集简单的剖析信息,AOT分层编译的代码也是如此。当AOT调用达到一定阈值,这些方法会在第3层被客户端编译器编译,这也为将在第4层发生的服务端再编译收集了全部剖析信息。

通过探针技术,实现Java应用程序自我防护

解释执行:将编译好的字节码一行一行地翻译为机器码执行。

编译执行:以方法为单位,将字节码一次性翻译为机器码后执行。

该提案由HotSpot团队负责人Valdimir
Kozlov提交,里面提到了在第一个版本里只有java.base模块支持多层AOT,因为这个基本模块为众人所知,可以得到全面的内部测试。

新Java,新未来

  

AOT带来了一个叫作“jaotc”的工具,它在内部使用了Graal(用于生成代码)。Graal动态编译器集成了HotSpot虚拟机并且依赖JVM编译器接口(JVMCI),所以JDK(支持Graal或AOT)应该也支持JVMCI。Oracle
technetwork网站上就有一些支持JVMCI的JDK版本。

针对容器化服务的分布式存储实践

  在编译示时期,我们通过将源代码编译成.class
,配合JVM这种跨平台的抽象,屏蔽了底层计算机操作系统和硬件的区别,实现了“一次编译,到处运行”
。 而在运行时期,目前主流的JVM 都是混合模式(-Xmixed),即解释运行
和编译运行配合使用。

根据提案的描述,jaotc工具支持以下这些标记:

分布式关系型数据库架构探索

  以 Oracle
JDK提供的HotSpot虚拟机为例,在HotSpot虚拟机中,提供了两种编译模式:解释执行

即时编译(JIT,Just-In-Time)。解释执行即逐条翻译字节码为可运行的机器码,而即时编译则以方法为单位将字节码翻译成机器码(上述提到的“编译执行”)。前者的优势在于不用等待,后者则在实际运行当中效率更高。

--module  Module to compile
--output  Output file name
--compile-commands  Name of file with compile commands
--compile-for-tiered Generated profiling code for tiered compilation
--classpath  Specify where to find user class files
--threads  Number of compilation threads to be used
--ignore-errors Ignores all exceptions thrown during class loading
--exit-on-error Exit on compilation errors
--info Print information during compilation
--verbose Print verbose information
--debug Print debug information
--help Print this usage message
--version Version information
-J Pass  directly to the runtime system

互联网金融的性能微创新,给你奇思妙想!

相关赞助商

服务端或C2编译器,它的编译临界值比较高,达到了10000,这有助于针对性能关键的方法生成高度优化的代码,这些方法由应用的关键执行路径来判定是否属于性能关键方法。

  即时编译存在的意义在于它是提高程序性能的重要手段之一。根据“二八定律”(即:百分之二十的代码占据百分之八十的系统资源),对于大部分不常用的代码,我们无需耗时间将之编译为机器码,而是采用解释执行的方式,用到就去逐条解释运行;对于一些仅占据小部分的热点代码(可认为是反复执行的重要代码),则可将之翻译为符合机器的机器码高效执行,提高程序的效率,此为运行时的即时编译。

产品级的JVM有如下标记:

分层编译的五个层次

通过引进分层编译,OpenJDK HotSpot VM
用户可以通过使用服务端编译器改进启动时间获得好处。分层编译有五个编译层次。在第0层(解释层)启动,仪表在这一层提供了性能关键方法的信息。很快就会
到达第1层,简单的C1(客户端)编译器,它来优化这段代码。在第一层没有性能优化的信息。下面来到第2层,在此只有少数方法是编译过的(再提一下是通过
客户端编译器)。在第2层,为这些少数方法针对进入次数和循环分支收集性能分析信息。第3层将会看到由客户端编译器编译的所有方法及其全部性能优化信息,
最后的第4层只对C2自身有效,是服务端编译器。

  为了满足不同的场景,HotSpot虚拟机内置了多个即时编译器:C1,C2与Graal。Graal
是Java10正式引入的实验性即时编译器,在此暂不叙述(其实我不是很了解,尴尬···)。先看一下C1、C2
,相信大家或多或少接触过。

+/-UseAOT            - Use AOT-compiled files
+/-PrintAOT          - Print used AOT klasses and methods
AOTLibrary=    - Specify the AOT library file

分层编译器以及代码缓存的效果

当使用客户端编译(第2层之前)时,代码在启动期间通过客户端编译器予以优化,此时关键执行路径保持预热。这有助于生成比解释型代码更好的性能优化信息。编译的代码存在在一个称为“代码缓存”的缓存里。代码缓存有固定的大小,如果满了,JVM将停止方法编译。

分层编译可以针对每一层设定它自己的临界值,比如-XX:Tier3MinInvocationThreshold,
-XX:Tier3CompileThreshold,
-XX:Tier3BackEdgeThreshold。第三层最低调用临界值为100。而未分层的C1的临界值为1500,与之对比你会发现会非常频繁
地发生分层编译,针对客户端编译的方法生成了更多的性能分析信息。于是用于分层编译的代码缓存必须要比用于不分层的代码缓存大得多,所以在OpenJDK
中用于分层编译的代码缓存默认大小为240MB,而用于不分层的代码缓存大小默认只有48MB。

如果代码缓存满了,JVM将给出警告标识,鼓励用户使用
–XX:ReservedCodeCacheSize 选项去增加代码缓存的大小。

    C1:即Client编译器,面向对启动性能有要求的客户端GUI程序,采用的优化手段比较简单,因此编译的时间较短。

一些非产品级或用于开发的标记对用户也是可用的:

理解编译

为了可视化什么方法会在何时得到编译,OpenJDK HotSpot
VM提供了一个非常有用的命令行选项,叫做-XX:+PrintCompilation,它会报告什么时候代码缓存满了,以及什么时候编译停止了。

举例如下:

567  693 % !   3       org.h2.command.dml.Insert::insertRows @ 76 (513 bytes)
656  797  n    0       java.lang.Object::clone (native)  
779  835  s           4       java.lang.StringBuffer::append (13 bytes)

上面的输出格式为:

timestamp compilation-id flags tiered-compilation-level class:
method <@ osr_bci> code-size <deoptimization>

在此,

timestamp(时间戳) 是JVM开始启动到此时的时间

compilation-id(编译器id) 是内部的引用id

flags(标记) 可以是以下其中一种:

%: is_osr_method (是否osr方法@ 针对OSR方法表明字节码)
s: is_synchronized(是否同步的)
!: has_exception_handler(有异常处理器)
b: is_blocking(是否堵塞)
n: is_native(是否原生)

tiered-compilation(分层的编译器) 表示当开启了分层编译时的编译层

Method(方法) 将用以下格式表示类和方法 类名::方法

@osr_bci(osr字节码索引) 是OSR中的字节码索引

code-size(代码大小) 字节码总大小

deoptimization(逆优化)表示一个方法是否是逆优化,以及不会被调用或是僵尸方法(更多详细内容请见“动态逆优化”一节)。

基于以上关键字,我们可以断定例子中的第一行

567  693 % !  3  org.h2.command.dml.Insert::insertRows @ 76 (513 bytes)

的timestamp是567,compilation-ide是693。该方法有个以“!”标明的异常处理器。我们还能断定分层编译处于第3层,
它是一个OSR方法(以“%”标识的),字节码索引为76。字节码总大小为513个字节。请注意513个字节是字节码的大小而不是编译码的大小。

示例的第2行显示:

656  797  n 0 java.lang.Object::clone (native) 

JVM使一个原生方法更容易调用,第3行是:

779  835  s 4 java.lang.StringBuffer::append (13 bytes)

显示这个方法是在第4层编译的且是同步的。

动态逆优化

我们知道Java会做动态类加载,JVM在每次动态类加载时检查内部依赖。当不再需要一个之前优化过的方法时,OpenJDK
HotSpot
VM将执行该方法的动态逆优化。自适应优化有助于动态逆优化,换句话说,一个动态逆优化的代码应恢复到它之前编译层,或者转到新的编译层,如下图所示。
(注意:当在命令行中开启PrintCompilation时会输出如下信息):

 573  704 2 org.h2.table.Table::fireAfterRow (17 bytes)
7963 2223 4 org.h2.table.Table::fireAfterRow (17 bytes)
7964  704 2 org.h2.table.Table::fireAfterRow (17 bytes) made not entrant
33547 704 2 org.h2.table.Table::fireAfterRow (17 bytes) made zombie

这个输出显示timestamp为7963,fireAfterRow是在第4层编译的。之后的timestamp是7964,之前在第2层编译的fireAfterRow没有进入。然后过了一会儿,fireAfterRow标记为僵尸,也就是说,之前的代码被回收了。

    C2:即Server编译器,面向对性能峰值有要求的服务端程序,采用的优化手段复杂,因此编译时间长,但是在运行过程中性能更好。

PrintAOTStatistics   - Print AOT statistics
UseAOTStrictLoading  - Exit the VM if any of the AOT libraries has invalid config

理解内联

自适应优化的最大一个好处是有能力内联性能关键的方法。通过把调用替换为实际的方法体,有助于规避调用这些关键方法的间接开销。针对内联有很多基于规模和调用临界值的“协调”选项,内联已经得到了充分地研究和优化,几乎已经挖掘出了最大的潜力。

如果你想投入时间看一下内联决策,可以使用一个叫做-XX:+PrintInlining的JVM诊断选项。在理解决策时PrintInlining会提供很大的帮助,示例如下:

@ 76 java.util.zip.Inflater::setInput (74 bytes) too big
@ 80 java.io.BufferedInputStream::getBufIfOpen (21 bytes) inline (hot)
@ 91 java.lang.System::arraycopy (0 bytes)   (intrinsic)
@ 2  java.lang.ClassLoader::checkName (43 bytes) callee is too large

在这里你能看到该内联的位置和被内联的总字节数。有时你看到如“too
big”或“callee is too
large”的标签,这表明因为已经超过临界值所以未进行内联。第3行的输出信息显示了一个“intrinsic”标签,让我们在下一节详细了解一下
intrinsics(内部函数)。

  从Java7开始,HotSpot虚拟机默认采用分层编译的方式:热点方法首先被C1编译器编译,而后
热点方法中的热点再进一步被C2编译(理解为二次编译,根据前面的运行计算出更优的编译优化)。为了不干扰程序的正常运行,JIT编译时放在额外的线程中执行的,HotSpot根据实际CPU的资源,以
1:2的比例分配给C1和C2线程数。在计算机资源充足的情况,字节码的解释运行和编译运行时可以同时进行,编译执行完后的机器码会在下次调用该方法时启动,已替换原本的解释执行(意思就是已经翻译出效率更高的机器码,自然替换原来的相对低效率执行的方法)。

提案同时提到,AOT的运行时事件日志将集成统一GC日志,并支持如下标签:

内部函数

通常OpenJDK HotSpot VM
即时编译器将执行为性能关键方法生成的代码,但有时有些方法有非常公共的模式,比如java.lang.System::arraycopy,如前一节中PrintInlining输出的结果。这些方法可以得到手工优化从而形成更好的性能,优化的代码类似于拥有你的原生方法,但没有间接开销。这些内部函数可以高效地内联,就像JVM内联常规方法一样。

  以上,可以看出在Java中不单单是解释执行,即时编译(编译执行)在Java性能优化中彰显重要的作用,所以现在应该说:Java是解释执行和编译执行共同存在的,至少大部分是这样。

aotclassfingerprint
aotclassload
aotclassresolve

向量化

讨论内部函数的时候,我喜欢强调一个常用的编译优化,那就是向量化。向量化可用于任何潜在的平台(处理器),能处理特殊的并行计算或向量指令,比如
“SIMD”指令(单指令、多数据)。SIMD和“向量化”有助于在较大的缓存行规模(64字节)数据量上进行数据层的并行操作。

HotSpot VM提供了两种不同层次的向量支持:

在第一种情况下,在内部循环的工作过程中配备的桩能为内部循环提供向量支持,而且这个内部循环可以通过向量指令进行优化和替换。这与内部函数是类似的。

在HotSpot VM中SLP支持的理论依据是MIT实验室的一篇论文。目前,HotSpot
VM只优化固定展开次数的目标数组,Vladimir
Kozlov举了以下一个示例,他是Oracle编译团队的资深成员,在各种编译器优化作出了杰出贡献,其中就包括自动向量化支持。

a[j] = b + c * z[i]

如上代码展开之后就可以被自动向量化了。

逃逸分析

逃逸分析是自适应优化的另一个额外好处。为判定任何内存分配是否“逃逸”,逃逸分析(缩写为EA)会将整个中间表示图考虑进来。也就是说,任意内存分配是否不在下列之一:

如果已分配的对象不是逃逸的,编译的方法和对象不作为参数传递,那么该内存分配就可以被移除了,这个域的值可以存储在寄存器中。如果已分配的对象未逃逸已编译的方法,但作为参数传递了,JVM仍然可以移除与该对象有关联的锁,当用它比对其他对象时可以使用优化的比对指令。

  

不列出风险或没有基本假定的提案是不完整的,AOT也不例外。AOT提案的风险标注如下:

其他常见的优化

还有一些自适应即时编译器一起带来的一些其他的OpenJDK HotSpot VM优化:

  1. 循环展开——通过展开循环有助于减少迭代次数。这能使JVM有能力去应用其他常见的优化(比如循环向量化),无论哪里需要。

引用:

 

HotSpot
VM剖析,jitopenjdk 重点
应用程序可以选择一个适当的即时编译器来进行接近机器级的性能优化。…

PS:1. 如有错误,请务必指出,以免误导他人。

预编译的代码可能不是最优的,所以会导致性能损失。性能测试结果表明,有些应用程序会从AOT编译的代码中获益,不过有些却出现明显的性能衰退。因为AOT特性是可选的,所以应用程序出现的性能衰退是可以避免的。如果用户发现应用程序启动变慢,或者达不到预期的性能峰值,那么可以重新构建一个不包含AOT库的JDK。

   2.
第一次写博客,写得简陋,还请多多见谅。希望能巩固知识,分享知识,共同进步。

查看英文原文:Ahead-of-Time (AOT) Compilation May Come to OpenJDK
HotSpot in Java
9

 

来自:InfoQ

作者 Monica
Beckwith ,译者 薛命灯