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

Ada*_*ode 48 java jit

我听说Java必须使用JIT才能快速.与解释相比,这是完全合理的,但为什么不能有人制作生成快速Java代码的提前编译器?我知道gcj,但我认为它的输出通常不比Hotspot快.

是否有关于语言的事情使这个困难?我认为这取决于这些事情:

  • 反射
  • 类加载

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

And*_*are 37

JIT编译器可以更快,因为机器代码是在它也将执行的确切机器上生成的.这意味着JIT具有可用于发出优化代码的最佳信息.

如果将字节码预编译为机器代码,则编译器无法针对目标机器(仅针对构建机器)进行优化.

  • 提前编译器仍然可以匹配.例如,可以告诉英特尔C++编译器发出一段代码的多个版本,每个版本都针对略有不同的处理器目标进行调整.它将添加代码以在启动时自动检测处理器并选择最合适的代码路径. (12认同)
  • 对于大多数程序,机器特定的优化并不重要。我认为这不是 JIT 的重点。 (2认同)

Edw*_*rzo 29

我将粘贴一本有趣的答案,由詹姆斯·戈斯林(James Gosling)在"书籍编程大师"中作出.

好吧,我听说它说有效地在Java世界中有两个编译器.你有编译器到Java字节码,然后你有你的JIT,它基本上再次重新编译所有内容.所有可怕的优化都在JIT中.

詹姆斯:没错.这些天我们总是打败真正优秀的C和C++编译器.当你转到动态编译器时,当编译器在最后一刻运行时,你会获得两个优势.一个是你确切知道你正在运行的芯片组.很多时候,当人们编译一段C代码时,他们必须编译它以在通用x86架构上运行.你得到的二进制文件几乎都没有特别适合它们.您下载了Mozilla的最新副本,它几乎可以在任何英特尔架构CPU上运行.几乎有一个Linux二进制文件.它非常通用,它是用GCC编译的,它不是一个非常好的C编译器.

当HotSpot运行时,它确切地知道您正在运行的芯片组.它确切知道缓存的工作原理.它确切地知道内存层次结构如何工作.它确切地知道所有管道互锁在CPU中如何工作.它知道这个芯片有哪些指令集扩展.它可以精确地优化您所使用的机器.然后另一半是它实际上看到应用程序正在运行.它能够获得知道哪些事情很重要的统计数据.它能够内联C编译器永远不会做的事情.在Java世界中内联的东西非常惊人.然后,您将了解存储管理与现代垃圾收集器的工作方式.使用现代垃圾收集器,存储分配非常快.

编程的主人

  • 这是有趣的。我如何在运行的计算机上完全编译程序,所以性能会很好,并且不需要整个VM都在运行。由于我们在大多数机器都是相同的环境中运行,因此这种情况经常发生。 (2认同)

Luk*_*ane 23

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

懒惰它只会在到达它们时编译方法而不是编译整个程序(如果你不使用程序的一部分,这非常有用).类加载实际上允许它忽略它尚未遇到的类,从而有助于使JIT更快.

自适应

自适应它首先发出一个快速和脏的机器代码版本,然后只返回并完成一个直接作业,如果经常使用该方法.

  • 其自适应性的另一个方面是,它可以在解释字节码时收集测试/分支的可能结果的统计数据,并将其提供给JIT编译器以生成更好的代码. (11认同)

Tho*_*sen 23

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

Class.forName(...)
Run Code Online (Sandbox Code Playgroud)

这意味着您无法编写涵盖所有 Java程序的AOT编译器,因为只有在运行时才能获得有关程序特征的信息.但是,您可以在Java的子集上执行此操作,这是我认为gcj所做的.

另一个典型的例子是JIT能够直接在调用方法中内联getX()方法,如果发现它是安全的,并在适当的情况下撤消它,即使程序员没有通过告诉方法是最终的.JIT可以看到在运行程序中,给定的方法没有被覆盖,因此在这种情况下可以被视为final.在下次调用时可能会有所不同.

  • __known__类加载器的Class.forName()可以通过解析为预编译类来处理.我们为Eclipse RCP和Tomcat类加载器做了这些,更不用说系统和应用程序:http://www.excelsiorjet.com (7认同)

Tal*_*man 11

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

  • LLVM 也有相同的信息,奇怪的是 Linux 也有。通过在目标机器上编译代码,您可以获得相同或类似的好处。 (2认同)

Sam*_*ell 6

Java能够跨越虚拟方法边界并执行有效的接口调度,这需要在编译之前进行运行时分析 - 换句话说,它需要JIT.由于所有方法都是虚拟的,接口"无处不在",因此它会产生很大的不同.


Dmi*_*kov 6

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

现在,如果你有一个桌面应用程序,快速启动和初始响应时间(AOT闪耀)等事情变得更加重要,而且计算机可能没有足够的资源用于最高级的优化.

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

但是,以上都是理论.实际上,创建这样一个高级JIT编译器比一个体面的AOT编译器更复杂.一些实际证据怎么样?


gus*_*afc 6

JIT可以识别并消除一些只能在运​​行时知道的条件.一个主要的例子是消除现代VM使用的虚拟调用 - 例如,当JVM找到invokevirtualinvokeinterface指令时,如果只加载了一个覆盖调用方法的类,则VM实际上可以使该虚拟调用保持静态,从而能够内联它.另一方面,对于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();
Run Code Online (Sandbox Code Playgroud)

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