gpt4 book ai didi

java - 为什么Java Just in Time Compiler会继续重新编译相同的方法并使方法成为非租用的

转载 作者:行者123 更新时间:2023-12-01 09:52:33 24 4
gpt4 key购买 nike

我在Windows上使用AdoptJDk 11.0.7 Java,并启用了-XX:+ PrintCompilation标志,因此我可以看到正在编译的方法,而只是解释了

我正在应用程序中调用某些功能(该功能处理音频文件并在文件上创建html报告)。我启动了应用程序一次(虽然GUI有限),然后在同一组文件上运行相同任务多次。第二次调用它的运行速度比第一次快得多,第三次比第二次要快一些,因此后续运行之间没有太大差异。但是我注意到,每次运行时,它仍在编译许多方法,并且许多方法变得不可重入。

它是分层编译的,所以我知道可以将同一方法重新编译到更高的级别,但是正在编译的方法数量似乎并没有太大变化。

我不明白为什么这么多方法变成不可重入的(然后是僵尸),我还没有做详细的分析,但是似乎一遍又一遍地编译了相同的方法,为什么呢?

我添加了-XX:-BackgroundCompilation选项,以强制按顺序编译方法,并使代码等待编译版本,而不是在编译时使用解释版本。这似乎减少了可重入方法的数量,也许这是因为它减少了多个线程尝试访问(重新)编译的方法的机会?

但是仍然有很多方法可以重新编译

例如,在这里我可以看到它被编译为3级,然后被编译为4级,因此3级编译成为不可进入的且僵化了。但是随后第4级变得不可重入,然后又回到第4级进行编译,依此类推。

enter image description here

最佳答案

简短的答案是,JIT取消优化会导致禁用已编译的代码(“不让其进入”),释放该代码(“使不让僵尸”)并在再次调用时重新编译(足够的次数)。

JVM方法高速缓存维护四个状态:

enum {
in_use = 0, // executable nmethod
not_entrant = 1, // marked for deoptimization but activations
// may still exist, will be transformed to zombie
// when all activations are gone
zombie = 2, // no activations exist, nmethod is ready for purge
unloaded = 3 // there should be no activations, should not be
// called, will be transformed to zombie immediately
};

一个方法可以是 in_use,它可能已被取消优化禁用( not_entrant),但仍可以调用,或者可以将其标记为 zombie,如果它是 non_entrant且不再使用。最后,可以将该方法标记为要卸载。

如果是分层编译,则根据使用情况统计信息,客户端编译器(C1)生成的初始编译结果可能会被服务器编译器(C2)的编译结果替换。
-XX:+PrintCompilation输出中的编译级别范围从 040代表解释, 13代表客户端编译器的不同优化级别, 4代表服务器编译器。在输出中,您可以看到 java.lang.String.equals()3过渡到 4。发生这种情况时,原始方法将标记为 not_entrant。仍然可以调用它,但是一旦不再引用它,它将转换为 zombie

JVM清除程序( hotspot/share/runtime/sweeper.cpp)是一项后台任务,负责管理方法生命周期并将 not_reentrant方法标记为 zombie。扫描间隔取决于许多因素,一个是方法缓存的可用容量。低容量将增加背景扫描的次数。您可以使用 -XX:+PrintMethodFlushing监视清除 Activity (仅JVM调试版本)。可以通过最小化缓存大小并最大化其攻击性阈值来提高扫描频率:
-XX:StartAggressiveSweepingAt=100 (JVM debug builds only)
-XX:InitialCodeCacheSize=4096 (JVM debug builds only)
-XX:ReservedCodeCacheSize=3m (JVM debug builds noly)

为了说明生命周期,可以将 -XX:MinPassesBeforeFlush=0(仅JVM调试版本)设置为强制立即转换。

下面的代码将触发以下输出:
while (true) {
String x = new String();
}
    517   11    b  3       java.lang.String::<init> (12 bytes)
520 11 3 java.lang.String::<init> (12 bytes) made not entrant
520 12 b 4 java.lang.String::<init> (12 bytes)
525 12 4 java.lang.String::<init> (12 bytes) made not entrant
533 11 3 java.lang.String::<init> (12 bytes) made zombie
533 12 4 java.lang.String::<init> (12 bytes) made zombie
533 15 b 4 java.lang.String::<init> (12 bytes)
543 15 4 java.lang.String::<init> (12 bytes) made not entrant
543 13 4 java.lang.String::<init> (12 bytes) made zombie
java.lang.String的构造函数先用C1编译,然后用C2编译。 C1的结果被标记为 not_entrantzombie。后来,对于C2结果也是如此,此后将进行新的编译。

即使以前已成功编译该方法,但所有先前结果的 zombie状态都将触发新的编译。因此,这可能会反复发生。取决于主要因素, zombie状态可能会延迟(根据您的情况),具体取决于已编译代码的年龄(通过 -XX:MinPassesBeforeFlush控制),方法缓存的大小和可用容量以及 not_entrant方法的使用。

现在,我们知道这种连续的重新编译很容易发生,就像您的示例中一样( in_use-> not_entrant-> zombie-> in_use)。但是,除了从C1过渡到C2之外,什么还可以触发 not_entrant,方法使用期限约束和方法缓存大小矛盾,以及如何可视化推理?

使用 -XX:+TraceDeoptimization(仅JVM调试版本),您可以了解将给定方法标记为 not_entrant的原因。在上述示例的情况下,输出为(为便于阅读而缩短/重新格式化):
Uncommon trap occurred in java.lang.String::<init>
reason=tenured
action=make_not_entrant

在这里,原因是 -XX:MinPassesBeforeFlush=0施加了年龄限制:
Reason_tenured,               // age of the code has reached the limit

JVM知道取消优化的以下其他主要原因:
Reason_null_check,            // saw unexpected null or zero divisor (@bci)
Reason_null_assert, // saw unexpected non-null or non-zero (@bci)
Reason_range_check, // saw unexpected array index (@bci)
Reason_class_check, // saw unexpected object class (@bci)
Reason_array_check, // saw unexpected array class (aastore @bci)
Reason_intrinsic, // saw unexpected operand to intrinsic (@bci)
Reason_bimorphic, // saw unexpected object class in bimorphic
Reason_profile_predicate, // compiler generated predicate moved from
// frequent branch in a loop failed

Reason_unloaded, // unloaded class or constant pool entry
Reason_uninitialized, // bad class state (uninitialized)
Reason_unreached, // code is not reached, compiler
Reason_unhandled, // arbitrary compiler limitation
Reason_constraint, // arbitrary runtime constraint violated
Reason_div0_check, // a null_check due to division by zero
Reason_age, // nmethod too old; tier threshold reached
Reason_predicate, // compiler generated predicate failed
Reason_loop_limit_check, // compiler generated loop limits check
// failed
Reason_speculate_class_check, // saw unexpected object class from type
// speculation
Reason_speculate_null_check, // saw unexpected null from type speculation
Reason_speculate_null_assert, // saw unexpected null from type speculation
Reason_rtm_state_change, // rtm state change detected
Reason_unstable_if, // a branch predicted always false was taken
Reason_unstable_fused_if, // fused two ifs that had each one untaken
// branch. One is now taken.

有了这些信息,我们可以继续研究与 java.lang.String.equals()直接相关的更有趣的示例-您的场景:
String a = "a";
Object b = "b";
int i = 0;
while (true) {
if (++i == 100000000) {
System.out.println("Calling a.equals(b) with b = null");
b = null;
}
a.equals(b);
}

该代码通过比较两个 String实例开始。经过一亿次比较,它将 b设置为 null并继续。这就是此时发生的情况(为了便于阅读,对其进行了缩短/重新格式化):
Calling a.equals(b) with b = null
Uncommon trap occurred in java.lang.String::equals
reason=null_check
action=make_not_entrant
703 10 4 java.lang.String::equals (81 bytes) made not entrant
DEOPT PACKING thread 0x00007f7aac00d800 Compiled frame
nmethod 703 10 4 java.lang.String::equals (81 bytes)

Virtual frames (innermost first):
java.lang.String.equals(String.java:968) - instanceof @ bci 8

DEOPT UNPACKING thread 0x00007f7aac00d800
{method} {0x00007f7a9b0d7290} 'equals' '(Ljava/lang/Object;)Z'
in 'java/lang/String' - instanceof @ bci 8 sp = 0x00007f7ab2ac3700
712 14 4 java.lang.String::equals (81 bytes)

根据统计信息,编译器确定可以消除 instanceof( java.lang.String.equals())使用的 if (anObject instanceof String) {中的null检查,因为 b绝不会为null。经过一亿次运算后,该不变式被违反,并且陷阱被触发,从而导致使用空检查进行重新编译。

我们可以通过以 null开头并在一亿次迭代后分配 b来扭转局面,以说明另一个非优化原因:
String a = "a";
Object b = null;
int i = 0;
while (true) {
if (++i == 100000000) {
System.out.println("Calling a.equals(b) with b = 'b'");
b = "b";
}
a.equals(b);
}
Calling a.equals(b) with b = 'b'
Uncommon trap occurred in java.lang.String::equals
reason=unstable_if
action=reinterpret
695 10 4 java.lang.String::equals (81 bytes) made not entrant
DEOPT PACKING thread 0x00007f885c00d800
nmethod 695 10 4 java.lang.String::equals (81 bytes)

Virtual frames (innermost first):
java.lang.String.equals(String.java:968) - ifeq @ bci 11

DEOPT UNPACKING thread 0x00007f885c00d800
{method} {0x00007f884c804290} 'equals' '(Ljava/lang/Object;)Z'
in 'java/lang/String' - ifeq @ bci 11 sp = 0x00007f88643da700
705 14 2 java.lang.String::equals (81 bytes)
735 17 4 java.lang.String::equals (81 bytes)
744 14 2 java.lang.String::equals (81 bytes) made not entrant

在这种情况下,编译器确定与 instanceof条件( if (anObject instanceof String) {)相对应的分支永远不会被采用,因为 anObject始终为空。可以消除包括条件在内的整个代码块。在进行了1亿次运算后,该不变式被违反,并且陷阱被触发,从而导致重新编译/解释而没有消除分支。

编译器执行的优化基于代码执行期间收集的统计信息。优化器的假设通过陷阱进行记录和检查。如果违反了这些不变式中的任何一个,则会触发陷阱,从而导致重新编译或解释。如果执行模式更改,则即使存在先前的编译结果,也可能会触发重新编译。如果出于上述原因将编译结果从方法缓存中删除,则可能会再次为受影响的方法触发编译器。

关于java - 为什么Java Just in Time Compiler会继续重新编译相同的方法并使方法成为非租用的,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61890873/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com