【问题标题】:Why doesn't the JVM compile the entire program up front, instead of compiling it piece-by-piece?为什么 JVM 不预先编译整个程序,而是逐段编译?
【发布时间】:2016-05-30 08:43:53
【问题描述】:

Herbert Schildt 对此线程写道:

了解将整个 Java 程序一次全部编译成可执行代码是不切实际的,因为 Java 执行只能在运行时进行的各种运行时检查,这一点很重要。

他的意思是什么运行时检查?

请解释编译字节码的原因,而不是整个程序。

【问题讨论】:

  • 我不完全确定这是关于 *.java -> *.class 还是关于 JIT 编译,有人能澄清一下吗?
  • 这是关于JIT编译的。

标签: java jvm compilation jit bytecode


【解决方案1】:

逐段编译可能有几个原因(这是我想到的前两个):

  1. 多次使用的代码的优化,不是所有的代码都需要重新编译,只需要重新编译特定的部分。
  2. 通过网络获取类 - 有时您希望避免获取所有代码,因为这会占用带宽。

而且我不知道这个网站是否准确,但我从中学到了很多:http://www.artima.com/insidejvm/ed2/index.html

【讨论】:

    【解决方案2】:

    他的意思是在运行时将所有字节码编译成机器语言是不切实际的。您可以预编译所有内容,但这不是 JIT 采用的方法。

    一方面,不知道程序有多大。人们会在 30 分钟的启动过程中感到相当不安,因为它编译了它可以找到的每个库(给定的 Java 程序不在单个文件中,它可以访问类路径中的所有内容)

    另一方面,即使您确切地告诉系统您的程序将使用哪些组件,也不知道您的程序有多少可以用于给定的运行 - 人们会在 30 分钟的启动运行时更加不安命令行程序,参数由“--help”组成

    最后,它可以通过在运行时进行编译来完成一些很棒的技巧。用这样的方法:

    public testMeh(int param) {
        if(param == 35) 
            do bunches of crap;
        else if (param > 5)
            do much more crap;
        else 
            return;
        }
    

    编译器可以调用它一次或两次,并在运行中识别出值 5 及以下的值仅返回。如果它一直以 2 的值被调用,它可以用 if (param != 2) testMeh(param); 替换整个方法调用;

    这消除了对该数字的整个方法调用。稍后它会发现,不调用该方法意味着某些成员变量无法更改,并且可以将代码的其他部分折叠为空。

    如果你预编译一些东西,这简直是太难了。我的意思是您可以在识别模式时在任何地方编写异常代码,但您的代码很快就会变成一场噩梦。

    现在,如果您问为什么在将整个程序编译成字节码时不对其进行预编译——这是一个不同的问题,而不是引用的内容——但你可以这样做。它已经完成并且效果很好。您可以用可移植性和运行时灵活性换取更快的启动时间。

    【讨论】:

    • 很棒的答案,有实际的例子。
    【解决方案3】:

    一个可能的原因:Java 运行时是一个非常动态的环境。类一直在加载和卸载。根据当前使用的类,某些优化是可能的或不可能的。例如,如果您有接口 A 和单个实现 ImplA,则对接口 A 中的所有方法的调用都可以更改为使用对 ImplA 方法的直接调用,而无需执行任何虚拟方法。一旦加载了另一个类 AnotherImplA,这种优化就不再可能了。一旦卸载了 AnotherImplA,就可以再次执行。

    JVM 中有很多优化使用在运行时收集的数据。预先进行所有优化会错过很多机会。

    查看 Cliff Click 最近的谈话,A JVM Does That?

    【讨论】:

      【解决方案4】:

      这个推理是不正确的。可以一次将整个 Java 程序编译成可执行代码,事实上gcj 正是这样做的。

      Java 运行时通常不进行预先编译的实际原因:

      • 作为 Java “一次编写,随处运行”理念的一部分,编译后的代码作为独立于平台的字节码而不是特定于平台的本机代码分发。

      • 在启动时将字节码编译为本机代码并非最佳选择,原因有几个。首先,它会产生相当高的启动成本,尤其是对于大型项目。此外,现代 JIT 能够利用运行时分析来产生比使用预先编译器更好的优化。优化通常涉及权衡取舍:例如,内联消除了函数调用开销,但代价是更大的代码(由于缓存性能差甚至交换,这反过来会导致代码变慢)。当您在编译时获得运行时分析信息时,您可以更明智地决定哪些权衡是有意义的。

      • Java API 历来依赖于在运行时加载字节码的能力。 (想想任何依赖于Class.forName 和/或自定义类加载器的东西,例如,包括小程序、servlet 容器等)为了支持这一点,某种运行时编译器(或解释器)是必要的。进行预先编译意味着您要么必须删除对此的支持,要么将其视为特殊情况。

      【讨论】:

      • 可能!=实用。我不知道 gcj 生成什么代码,但我必须假设它要么被对执行这些检查的函数的调用所淹没,要么被这些测试的内联版本所淹没。 JIT 的代码可以假设一切正常并留下一个小守卫,这通常效率更高。
      • @delnan:对不起,我还没来得及写下我的答案,就被现实世界叫走了。我的观点是关于使用的推理,而不是结论。 JIT 无法使用魔法。 JIT 可以使用的任何类型的防护也可以由预先编译的代码使用,因此由于“Java 执行各种运行时检查”而预先编译 Java 是不切实际的论点是有缺陷的。 Java 通常不预先编译是有充分理由的,但运行时检查是一个红鲱鱼。
      【解决方案5】:

      启动时间更快,并且能够在执行期间分析代码后进行更智能的优化。

      这篇文章很好地回答了这个问题:https://advancedweb.hu/2016/05/27/jvm_jit_optimization_techniques/

      JVM 开始即时解释所有内容。它记录一个函数被调用了多少次,如果它超过了一定的限制,它就会编译那个函数并缓存这个机器代码。之后每次调用该函数时,它都会执行此机器代码。如果它超过另一个阈值,它可以再次编译,但执行更积极的优化。

      目标是平衡编译的前期成本。预先编译所有内容将导致更长的启动时间,并且编译器无法在没有看到代码运行的情况下进行最佳优化。与运行时间较短的程序相比,运行时间较长的程序具有更好的优化潜力。

      如果循环执行多次,它也可以只编译循环体。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-02-14
        • 1970-01-01
        • 1970-01-01
        • 2017-01-11
        • 1970-01-01
        相关资源
        最近更新 更多