在本章中,我们将学习JIT编译器,以及编译语言和解释语言之间的区别。
编译语言与解释语言
C,C++和FORTRAN等语言是编译语言。代码以二进制代码的形式提供,目标是底层机器。高级代码由专门为底层架构编写的静态编译器一次编译成二进制代码。生成的二进制文件不能在任何其他体系结构上运行。
另一方面,Python和Perl等解释语言可以在任何机器上运行,只要它们具有有效的解释器即可。它在高级代码上逐行进行,将其转换为二进制代码。
解释代码通常比编译代码慢。例如,考虑一个循环。解释将转换循环的每次迭代的相应代码。另一方面,编译代码翻译只生成一个二进制文件。解释器一次只能看到一行,因此无法执行任何重要的代码,例如,更改编译器等语句的执行顺序。
我们将在下面研究优化的例子 -
添加存储在内存中的两个数字。由于访问内存可能会消耗多个CPU周期,因此良好的编译器将发出指令以从内存中获取数据,并仅在数据可用时执行添加。它不会等待,同时执行其他指令。另一方面,在解释期间不可能进行这样的优化,因为解释器在任何给定时间都不知道整个代码。
但是,解释语言可以在任何具有该语言的有效解释器的机器上运行。
Java是编译还是解释语言?
Java试图找到一个中间立场。由于JVM位于javac编译器和底层硬件之间,因此javac(或任何其他编译器)编译器在Bytecode中编译Java代码,这是由特定于平台的JVM所理解的。然后,当代码执行时,JVM使用JIT(即时)编译以二进制编译字节码。
热点(HotSpots)
在典型的程序中,只有一小部分代码经常执行,而且通常,这段代码会显着影响整个应用程序的性能。这些代码段称为HotSpots。
如果某些代码段只执行一次,那么编译它将是一种浪费,而且解释字节码会更快。但是如果该部分是一个热点(HotSpots)部分并且执行多次,那么JVM将编译它。例如,如果一个方法被多次调用,那么编译代码所需的额外周期将生成的更快的二进制文件所抵消。
此外,JVM运行特定方法或循环越多,它收集的信息越多,以进行各种优化,从而生成更快的二进制文件。
考虑以下代码 -
for(int i = 0 ; I <= 100; i++) {
System.out.println(obj1.equals(obj2)); //two objects
}
Java
这个代码中,解释器会为每次迭代推导出obj1
类。这是因为Java中的每个类都有一个equals()
方法,它从Object
类扩展并可以覆盖。
另一方面,实际发生的是JVM会注意到,对于每次迭代,obj1
都是String
类,因此,它会直接生成与String
类的equals()
方法对应的代码。因此不需要查找,并且编译的代码将执行得更快。
只有当JVM知道代码的行为时,才能实现这种行为。因此,它在编译代码的某些部分之前等待。
以下是另一个例子 -
int sum = 7;
for(int i = 0 ; i <= 100; i++) {
sum += i;
}
Java
对于每个循环,解释器从内存中获取sum
的值,向其添加I
,并将其存储回内存。内存访问是一项昂贵的操作,通常需要多个CPU周期。由于此代码多次运行,因此它是HotSpot。JIT将编译此代码并进行以下优化。
sum
的本地副本将存储在特定于特定线程的寄存器中。所有操作都将对寄存器中的值进行,当循环完成时,该值将写回内存。
如果其他线程也在访问变量怎么办?由于某些其他线程正在对变量的本地副本进行更新,因此它们会看到过时的值。在这种情况下需要线程同步。一个非常基本的同步原语是将sum
声明为volatile
。在访问变量之前,线程将刷新其本地寄存器并从内存中获取值。访问后,该值立即写入内存。
以下是JIT编译器完成的一些常规优化 -
- 方法内联
- 死代码消除
- 用于优化调用站点的启发式算法
- 不断折叠