📜  Java虚拟机-JIT编译器

📅  最后修改于: 2020-11-13 05:35:40             🧑  作者: Mango


在本章中,我们将学习JIT编译器,以及编译和解释语言之间的区别。

编译语言与口译语言

诸如C,C++和FORTRAN之类的语言是已编译的语言。它们的代码以针对底层计算机的二进制代码形式提供。这意味着高级代码由专门为基础体系结构编写的静态编译器立即编译为二进制代码。生成的二进制文件不会在任何其他体系结构上运行。

另一方面,诸如Python和Perl之类的解释语言可以在任何机器上运行,只要它们具有有效的解释器即可。它逐行遍历高级代码,然后将其转换为二进制代码。

解释的代码通常比编译的代码慢。例如,考虑一个循环。解释器将为循环的每次迭代转换相应的代码。另一方面,编译后的代码只会使翻译一次。此外,由于解释器一次只能看到一行,因此它们无法执行任何重要的代码,例如更改诸如编译器之类的语句的执行顺序。

我们将在下面研究此类优化的示例-

将存储在内存中的两个数字相加。由于访问内存会占用多个CPU周期,因此优秀的编译器将发出指令以从内存中获取数据,并仅在数据可用时执行添加操作。它不会等待,同时执行其他指令。另一方面,由于解释器在任何给定时间都不了解整个代码,因此在解释期间无法进行这种优化。

但是,随后,解释型语言可以在具有该语言的有效解释器的任何计算机上运行。

Java已编译还是已解释?

Java试图找到中间立场。由于JVM位于javac编译器和基础硬件之间,因此javac(或任何其他编译器)编译器将Bytecode中的Java代码编译为平台特定的JVM可以理解。然后,随着代码执行,JVM使用JIT(即时)编译方式以二进制形式编译字节码。

热点

在典型的程序中,只有一小段代码会经常执行,而正是这种代码会显着影响整个应用程序的性能。这样的代码段称为HotSpots

如果某段代码仅执行一次,那么编译它会很浪费精力,而解释字节码会更快。但是,如果该节是一个热门节并且执行了多次,则JVM会编译它。例如,如果某个方法被多次调用,则编译代码所花费的额外周期将被生成的更快的二进制文件抵消。

此外,JVM运行特定方法或循环的次数越多,JVM收集的信息越多,它们将进行各种优化,从而生成更快的二进制文件。

让我们考虑以下代码-

for(int i = 0 ; I <= 100; i++) {
   System.out.println(obj1.equals(obj2)); //two objects
}

如果解释了此代码,则解释器将为每次迭代推导obj1类。这是因为Java中的每个类都有一个.equals()方法,该方法是从Object类扩展的并且可以被覆盖。因此,即使obj1是每次迭代的字符串,推演仍将完成。

另一方面,实际上会发生的情况是,JVM将注意到,每次迭代obj1都是String类的,因此,它将直接生成与String类的.equals()方法相对应的代码。因此,将不需要查找,并且编译后的代码将执行得更快。

仅当JVM知道代码的行为方式时,这种行为才可能发生。因此,它在编译代码的某些部分之前会等待。

以下是另一个示例-

int sum = 7;
for(int i = 0 ; i <= 100; i++) {
   sum += i;
}

对于每个循环,解释器都会从内存中获取“ sum”的值,并在其中添加“ I”,然后将其存储回内存中。内存访问是一项昂贵的操作,通常需要多个CPU周期。由于此代码多次运行,因此它是一个HotSpot。 JIT将编译此代码并进行以下优化。

“ sum”的本地副本将存储在特定于特定线程的寄存器中。所有操作都将对寄存器中的值完成,并且当循环完成时,该值将被写回到存储器中。

如果其他线程也正在访问该变量怎么办?由于其他线程正在对变量的本地副本进行更新,因此它们将看到过时的值。在这种情况下,需要线程同步。一个非常基本的同步原语是将“ sum”声明为volatile。现在,在访问变量之前,线程将刷新其本地寄存器并从内存中获取值。访问后,该值将立即写入存储器。

以下是JIT编译器完成的一些常规优化-

  • 方法内联
  • 消除死代码
  • 优化呼叫站点的启发式方法
  • 不断折叠