为什么Java使用JIT比编译机器代码更快?

为什么Java使用JIT比编译机器代码更快?,java,jit,Java,Jit,我听说Java必须使用JIT才能快速运行。与解释相比,这非常有意义,但为什么有人不能制作一个能够生成快速Java代码的提前编译器呢?我知道gcj,但我不认为它的输出通常比Hotspot更快 这门语言中有什么东西使它变得困难吗?我认为这可以归结为以下几点: 倒影 类加载 我错过了什么?如果我避免使用这些功能,是否可以将Java代码一次性编译为本机机器代码并完成此操作?JIT编译器可以更快,因为机器代码是在它也将在其上执行的机器上生成的。这意味着JIT拥有尽可能好的可用信息来发出优化的代码 如果

我听说Java必须使用JIT才能快速运行。与解释相比,这非常有意义,但为什么有人不能制作一个能够生成快速Java代码的提前编译器呢?我知道gcj,但我不认为它的输出通常比Hotspot更快

这门语言中有什么东西使它变得困难吗?我认为这可以归结为以下几点:

  • 倒影
  • 类加载

我错过了什么?如果我避免使用这些功能,是否可以将Java代码一次性编译为本机机器代码并完成此操作?

JIT编译器可以更快,因为机器代码是在它也将在其上执行的机器上生成的。这意味着JIT拥有尽可能好的可用信息来发出优化的代码


如果将字节码预编译为机器码,编译器将无法针对目标机器进行优化,只能针对生成机器进行优化。

Java跨虚拟方法边界内联并执行高效接口调度的能力需要在编译之前进行运行时分析,换句话说,它需要JIT。由于所有的方法都是虚拟的,并且接口“无处不在”,这就产生了很大的不同。

最后,它归结为一个事实,即拥有更多的信息可以实现更好的优化。在这种情况下,JIT有更多关于代码运行的实际机器的信息(正如Andrew所提到的),而且它还有很多在编译期间不可用的运行时信息。

Java的JIT编译器也是惰性和自适应的

懒惰的 由于懒惰,它只在到达方法时编译方法,而不是编译整个程序(如果不使用程序的一部分,则非常有用)。类加载允许JIT忽略尚未遇到的类,这实际上有助于提高JIT的速度

适应的
由于具有自适应性,它首先会发出一个快速而肮脏的机器代码版本,然后只有在频繁使用该方法时才会返回并完成一项完整的工作。

从理论上讲,如果JIT编译器有足够的时间和计算资源可用,它比AOT编译器具有优势。。例如,如果您的企业应用程序在具有大量RAM的多处理器服务器上运行数天或数月,那么JIT编译器可以生成比任何AOT编译器更好的代码

现在,如果你有一个桌面应用程序,那么像快速启动和初始响应时间(AOT发挥作用的地方)这样的事情就变得更加重要,而且计算机可能没有足够的资源进行最高级的优化

如果你有一个资源稀缺的嵌入式系统,JIT就没有机会对抗AOT


然而,以上都是理论。实际上,创建这样一个高级JIT编译器要比创建一个像样的AOT编译器复杂得多。一些怎么样?

任何AOT编译器的真正杀手是:

Class.forName(...)
这意味着您无法编写涵盖所有Java程序的AOT编译器,因为只有在运行时才有关于程序特征的信息可用。但是,您可以在Java的一个子集上实现它,我相信gcj就是这样做的

另一个典型的例子是,JIT能够在发现安全的情况下直接在调用方法中内联getX()等方法,并在适当的情况下撤销它,即使程序员没有明确地告诉它方法是最终的。JIT可以看到,在正在运行的程序中,给定的方法没有被重写,因此在本例中可以将其视为final。这在下一次调用中可能会有所不同



2019年编辑:Oracle引入了GraalVM,它允许在Java的子集(相当大,但仍然是一个子集)上进行AOT编译,主要要求所有代码在编译时可用。这允许web容器的启动时间为毫秒

我认为官方Java编译器是JIT编译器这一事实是其中的一个重要部分。与Java机器代码编译器相比,优化JVM花了多少时间?

JIT可以识别并消除一些只有在运行时才能知道的情况。一个主要的例子是现代虚拟机使用的虚拟调用的消除——例如,当JVM发现
invokevirtual
invokeinterface
指令时,如果只加载了一个覆盖调用方法的类,虚拟机实际上可以使该虚拟调用成为静态的,从而能够将其内联。另一方面,对于C程序,函数指针始终是函数指针,对它的调用不能内联(无论如何,在一般情况下)

以下是JVM能够内联虚拟调用的情况:

interface I { 
    I INSTANCE = Boolean.getBoolean("someCondition")? new A() : new B();
    void doIt(); 
}
class A implements I { 
    void doIt(){ ... } 
}
class B implements I { 
    void doIt(){ ... } 
}
// later...
I.INSTANCE.doIt();

假设我们不在别处创建
A
B
实例,并且
someCondition
设置为
true
,JVM知道对
doIt()
的调用总是意味着
A.doIt
,因此可以避免方法表查找,然后内联调用。在非JITted环境中,类似的构造是不可内联的。

我将把作者给出的有趣答案粘贴到书中

嗯,我听说过 实际上,您有两个编译器 Java世界。你有编译器 到Java字节码,然后 你的JIT,基本上是重新编译的 每件事都很明确。全部 你的可怕的优化在 准时制

詹姆斯:没错。这几天我们在 打败真正好的C和C++ 编译器几乎总是这样。当你 转到动态编译器,您将获得 当编译器 在最后一刻跑对了。一个 你知道确切的芯片组是什么吗 你在跑步。很多时候 人们正在编译一个C 代码,他们必须编译它才能运行 关于类泛型x86 建筑学几乎没有一个 你得到的二进制文件是特殊的