Refresh your Java skills–Java中的即时编译(Just-in-time compilation)
因自己在写的关于Java9的新书因为篇幅和读者层次的原因并不能将能想到的东西都写进去,故接下来整理出一系列的博文来补充拓展。
像其他一些编程语言一样,Java通常也被称为“编译语言”。但有时你可能会感到困惑,尤其是当有人告诉你Java是JIT编译,并问你其中的一些小细节时。
本文就来说一说JIT编译的概念。在第一部分,我们将对不同类型的编译描述一番。第二部分来说说JIT编译。接下来,我们将深入一下JIT编译在Java中比较特别的地方。
编译类型
在讨论编译类型之前,我们需要了解什么是编译。这是一个将编程语言翻译成机器可理解的语言(也称为机器代码)的过程。机器语言由CPU执行的指令组成。这个语言是由0-1构成的,如在wikibooks页面上的这个片段所示:
0001 00000111
0100 00001001
0000 00011110
即时编译
同样,我们知道,Java的javac指令不会生成机器代码,而是一些名为字节码的东西。而这不仅仅是一种语言会这么做(而这也是很多现代语言所发展的一个方向)。比如ActionScript(由ActionScript Virtual Machine执行)或CIL(由C#使用并在Common Language Runtime上执行)。
在这里,在我们的括号中所说的“执行”,也就是即时编译完成(即字节码编译成目标机器可执行的机器码)。这种特殊类型的编译发生在解释给定字节码的机器上,如ActionScript虚拟机或Java虚拟机(JVM)。字节码由他们在运行时( on runtime)编译成机器码。
这种编译带来了一些好处。第一个显着的优点是可以做到根据所运行机器参数来优化编译的代码。静态编译器为目标机器进行优化并一次生成机器代码。另一方面,JIT编译器提供了一种中间代码,它被转换和优化为特定于执行机器的机器代码。关于这里有一篇解释的比较通俗的文章动态编译和静态编译及Java执行,有兴趣可以看看
第二个优点是便携性。转换为字节码的代码可以在安装了虚拟机的任何计算机上运行。
Java中的即时编译
So,Java是即时编译为机器代码的。想要检查编译机器代码,我们可以启用多个JVM参数:
- -XX:+ PrintCompilation
通过这个参数,我们可以得到方法编译结果的输出。其输出的样例:
71 1 java.lang.String :: indexOf(70 bytes)
73 2 sun.nio.cs.UTF_8 $ Encoder :: encode(361 bytes)
87 3 java.lang.String :: hashCode(55 bytes)
- 输出被格式化为列,第一列(例如71)是时间戳。第二列返回唯一的编译器任务ID(1,2,3 …)。之后我们可以看到编译的方法。在括号中指定了编译字节码的字节。我们可以看到indexOf方法的大小是70字节,encode 方法是361字节等等。
- -XX:+ UnlockDiagnosticVMOptions
一个简单的标志,JVM诊断的补充选项。
- -XX:+ PrintInlining
通过这个配置,我们可以看到编译方法的细节。内联是编译器优化编译代码重要的工作方式。请看以下方法:
public void testMethod() {
callAnotherMethod();
}
通过内联,函数callAnotherMethod()将被callAnotherMethod的内容替换。正因为如此,在运行时,机器不会从一个方法跳转到另一个方法,并能够以内联方式执行代码。JIT通过此操作用来避免在堆栈上放置参数的复杂情况。当我们启用此参数(+PrintInlining)并运行代码时,我们可以看到类似下面的结果:
75 1 java.lang.String :: indexOf(70 bytes)
77 2 sun.nio.cs.UTF_8 $ Encoder :: encode(361 bytes)
@ 66 java.lang.String :: indexOfSupplementary(71 bytes) too big
@ 14 java.lang.Math :: min(11 bytes)(intrinsic)
@ 139 java.lang.Character :: isSurrogate(18 bytes) never executed
89 3 java.lang.String :: hashCode(55 bytes)
让我们回到理论层面面,Java中的JIT编译(这里说是动态编译)可以是(这里可以参考一篇文章
JVM即时编译(JIT),我这里用更加暴力通俗的方式说了下,能知道是个什么作用就可以):
- lazy:只有真正使用的方法(在运行时调用)才会被编译成机器代码。
- adaptive(自适应):整个程序被编译成一些脏机器代码。此代码仅针对非常常用的方法进行了优化。
已经编译的字节码存储到代码缓存中。这是一个结构,所有编译的方法。当再次调用给定方法时,它不会从头开始编译,而是从代码缓存中加载。但是,当编译器认为可以更好地优化此方法时,缓存方法可以被覆盖。在优化技术中,我们可以通过以下区分:
- 内联:在前面的描述中可以知道,可以避免方法跳跃。
- 垃圾代码(称之死代码更恰当):当某些对象存在于字节码中且不被使用时,编译器可以决定从机器代码中删除它们。
- 循环优化:编译器可以组织并优化循环执行顺序或对尾递归优化成for循环等,以此来优化CPU所执行的代码。
- 用实现方法替换接口方法:当给定接口的一个方法有且仅由一个对象实现时,编译器可以决定直接使用实现的方法,以避免在运行时绑定真正实现的方法所引起的开销。
在本文中,我们解释了即时编译,即特定用于语言的编译代码(如Java的字节码)转换为CPU可以理解的语言(机器代码)。编译器不会进行简单的编译,因为它也对编译代码进行了一些优化。由于这些优化,机器代码尽可能地适应目标机器,另外,可以根据 JVM调优系列:(一)什么是JVM – CSDN博客 ,这篇文章中的两张图来更好的理解下上面所说的一些细节。
原文:Refresh your Java skills–Java中的即时编译(Just-in-time compilation)