JVM如何决定对一个方法进行JIT编译(将方法归类为“热点方法”)?

43

我已经使用过-XX:+PrintCompilation,并且我知道JIT编译器的基本技术以及为什么要使用JIT编译。

然而,我仍然没有找到JVM如何决定何时对方法进行JIT编译的答案,即“JVM在何时将方法JIT编译”。

我是否正确地假设每个方法都会被解释,并且只有当它未被归类为“热方法”时才不会被编译?我记得曾经读到一个方法被执行了至少10,000次后就会被视为“热方法”,但我必须承认我不确定这是从哪里读到的。

因此,总结我的问题:

(1) 只要方法未被归类为“热方法”(因此已编译),它就会被解释吗?或者有其他原因使方法在不是“热方法”的情况下被编译吗?

(2) JVM如何将方法归类为“非热”和“热”方法?执行次数?还是其他什么?

(3) 如果存在某些阈值(例如执行次数)用于“热”方法,则是否有Java标志(-XX:...)来设置这些阈值?


看一下 -XX:+PrintFlagsFinal 的输出,有很多与JIT编译器及其层次、内联、方法大小、编译器线程等相关的标志。 - the8472
2个回答

81

HotSpot编译策略相当复杂,特别是对于默认启用的Java 8中的分层编译。这既不是执行次数,也不是CompileThreshold参数的问题。

最好的解释(似乎是唯一的合理解释)可以在HotSpot源代码中找到,请参见advancedThresholdPolicy.hpp

我将总结这种高级编译策略的主要观点:

  • 执行从第0层(解释器)开始。
  • 编译的主要触发器是:
    1. 方法调用计数器i
    2. 反向边计数器b。反向边通常表示代码中的循环。
  • 每当计数器达到某个频率值(TierXInvokeNotifyFreqLogTierXBackedgeNotifyFreqLog)时,就会调用编译策略来决定下一步该怎么做。根据ib的值以及当前C1和C2编译器线程的负载情况,可以决定:

    • 在解释器中继续执行;
    • 在解释器中开始分析;
    • 使用完整的配置文件数据在Tier 3中使用C1编译方法,以用于进一步重新编译;
    • 使用无配置文件但可以重新编译的可能性,在Tier 2中使用C1编译方法(不太可能);
    • 最后在Tier 1中使用C1编译方法,不带配置文件或计数器(同样不太可能)。

    这里的关键参数是TierXInvocationThresholdTierXBackEdgeThreshold。阈值可以根据编译队列的长度动态调整给定方法的值。

  • 编译队列不是FIFO,而是优先级队列。

  • 使用配置文件数据进行C1编译的代码(第3层),与解释执行类似,不同之处在于切换到下一层级(C2,第4层)的阈值更高。例如,经过约200次调用后,解释执行方法可以在第3层被编译,而经过5000多次调用后,C1编译的方法则需要在第4层重新编译。

  • 方法内联采用特殊策略。即使未被频繁调用,小型方法也可以内联到调用者中。稍大一些的方法只有在被频繁调用(InlineFrequencyRatio, InlineFrequencyCount)时才能被内联。

  • 4
    对我而言关键的见解是,正常路径是0(解释执行)→3(C1,全面分析)→4(C2)。在这个路径上,C1实际上只存在于为C2收集分析数据的目的。 - Tom Anderson
    8
    接下来有三个次要的备选路径。(1)如果C1编译器发现该方法很简单,则会在1 (C1,无剖析)进行编译,因为在4 (C2)下没有更快的速度。(2)如果C2编译器正在忙碌中,该方法将在2 (C1,轻剖析)处编译,直到C2不再忙碌,然后在3 C1(完全剖析)重新编译,以便它可以继续转到4 (C2)。(3)如果C1很忙但C2不忙,解释器将进行剖析,使该方法可以直接进入C2而不必经过C1。 - Tom Anderson
    3
    @apangin原来我们对于10,000次调用的一般知识(包括我的认知)都是错误的。感谢你提供出色的答案。 - Eugene
    1
    如果其他人正在寻找当前的实现,那么AdvancedThresholdPolicy已经合并到SimpleThresholdPolicy中。 - meriton
    1
    @jocull 编译器在第一层编译方法时,意味着HotSpot认为该方法不会从更高层次的编译中受益。通常这些是不需要复杂的C2优化的小而简单的方法。 - apangin
    显示剩余2条评论

    14

    控制这个的主要参数是-XX:CompileThreshold=10000

    Java 8的Hotspot现在默认使用分层编译,使用从级别1到4的多个编译阶段。我相信1表示没有优化。级别3是基于客户端的C1,级别4是基于服务器编译器的C2。

    这意味着一些小的优化可以比您预期的更早地发生,并且可以在达到10K阈值之后继续进行优化。我见过最高的是逃逸分析在一百万次调用后消除了一个StringBuilder。

    注意:循环迭代多次可能会触发编译器。例如,循环10K次的循环可能足够。

    1) 直到方法被认为足够热门之前,它都将被解释执行。但是,某些JVM(例如Azul Zing)可以在启动时编译方法,并且可以通过内部API强制Hotspot JVM编译方法。 Java 9可能也有一个AOT(提前)编译器,但据我所知,它仍在研究中。

    2) 调用次数或迭代次数。

    3) 是的-XX:CompileThreshold=是主要参数。


    你有更多的材料可以让我了解这个吗?我知道C1和C2,但我没有听说过编译级别1-4(解释器,C1,C2将是3个级别)。而且,你基本上说,一个方法在CompilerThreshold以下可能被认为是热点。那么这个阈值是什么呢?我可以期望一个方法最迟在调用CompilerThreshold次时被认为是热点吗? - Markus Weninger
    抱歉再问一次,为了确保我的术语正确:如果一个方法已经进行了至少一次优化,我可以将其称为“热”的吗?或者这正是“热方法”的定义? - Markus Weninger
    1
    @MarkusWeninger 这是每次更新都会发生变化的事情。如果您多次调用相同的代码集(但每次只调用一次),则可以看到它们不会全部同时编译。实际算法要复杂得多。我认为获取更多信息的最佳位置是源代码。其他任何写作可能已经过时了,无法适用于当前的JVM。 - Peter Lawrey
    1
    @MarkusWeninger 编译是在后台完成的,如果有很多方法需要编译,这可能需要相当长的时间。我会说一个方法是“热门”的,当它成为编译的候选时。确切地说,它何时被编译并替换现有代码是有点随机和难以控制的。 - Peter Lawrey
    1
    好的,谢谢你的信息!我会再等一段时间看看是否有更多的答案被发布,否则我会接受你的答案。 :) - Markus Weninger

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接