为什么 Java Just in Time Compiler 继续重新编译相同的方法并使方法不可租用
Why does Java Just in Time Compiler continue to recompile same methods and make methods non-rentrant
我在 Windows 上使用 AdoptJDk 11.0.7 Java 并启用了 -XX:+PrintCompilation 标志,所以我可以看到哪些方法是正在编译而不只是解释
我正在我的应用程序中调用一些功能(处理音频文件并创建文件的 html 报告)。我启动应用程序一次(GUI 有限),然后 运行 多次对同一组文件执行相同的任务。第二次调用它 运行s 比第一次快得多,第三次比第二次快一点,随后的 运行s 之间没有太大区别。但我注意到每个 运行 它仍在编译许多方法,并且很多方法都变得不可重入。
是分层编译,所以我明白同一个方法可以重新编译到更高的层次,但是编译的方法数量似乎没有太大变化。
我不明白为什么这么多方法变成不可重入(然后僵尸),我还没有做详细的分析,但似乎是相同的方法一遍又一遍地编译,为什么会这样?
我添加了 -XX:-BackgroundCompilation
选项来强制按顺序编译方法,并让代码等待编译版本,而不是在编译时使用解释版本。这似乎减少了 重入 方法的数量,所以这可能是因为它减少了多个线程尝试访问正在(重新)编译的方法的机会?
但似乎仍有许多方法需要重新编译
例如,在这里我可以看到它被编译到第 3 级,然后被编译到第 4 级,因此第 3 级编译成为非进入者和僵尸。但是随后第 4 级变得不可重入,它又回到第 4 级进行编译,依此类推。
简短的回答是,JIT 反优化会导致已编译的代码被禁用 ("made not entrant")、释放 ("made zombie"),并在再次调用时重新编译(足够次数)。
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
输出中的编译级别范围从 0
到 4
。 0
代表解释,1
到3
代表客户端编译器的不同优化级别,4
代表服务端编译器。在您的输出中,您可以看到 java.lang.String.equals()
从 3
过渡到 4
。发生这种情况时,原始方法被标记为 not_entrant
。它仍然可以被调用,但一旦不再被引用,它将转换为 zombie
。
JVM 清理器 (hotspot/share/runtime/sweeper.cpp
),一个后台任务,负责管理方法生命周期并将 not_reentrant
个方法标记为 zombie
s。清除间隔取决于多种因素,其中之一是方法缓存的可用容量。低容量会增加背景扫描的次数。您可以使用 -XX:+PrintMethodFlushing
(仅限 JVM 调试版本)监视扫描 activity。可以通过最小化缓存大小并最大化其攻击性阈值来增加扫描频率:
-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_entrant
和 zombie
。之后C2的结果也是如此,然后重新编译。
所有先前结果达到 zombie
状态会触发新的编译,即使该方法之前已成功编译。所以,这可能会一遍又一遍地发生。 zombie
状态可能会延迟(如您的情况),具体取决于编译代码的年龄(通过 -XX:MinPassesBeforeFlush
控制)、方法缓存的大小和可用容量以及 not_entrant
方法,命名主要因素。
现在,我们知道这种持续的重新编译很容易发生,就像您的示例中那样 (in_use
-> not_entrant
-> zombie
-> in_use
).但是除了从C1过渡到C2,方法年龄限制和方法缓存大小限制之外,还有什么可以触发not_entrant
以及如何可视化推理?
使用 -XX:+TraceDeoptimization
(仅限 JVM 调试构建),您可以了解给定方法被标记为 not_entrant
的原因。在上面的示例中,输出为(shortened/reformatted 为了便于阅读):
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
实例开始。经过 1 亿次比较后,它将 b
设置为 null
并继续。这就是当时发生的事情(shortened/reformatted 为了便于阅读):
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)
根据统计信息,编译器确定可以消除 java.lang.String.equals()
(if (anObject instanceof String) {
) 使用的 instanceof
中的空检查,因为 b
永远不会为空。经过 1 亿次操作后,不变量被违反,陷阱被触发,导致重新编译并进行空检查。
我们可以扭转局面,通过从 null
开始并在 1 亿次迭代后分配 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 亿次运算后,违反了该不变量并触发了陷阱,导致 recompilation/interpretation 没有分支消除。
编译器执行的优化基于代码执行期间收集的统计信息。优化器的假设通过陷阱记录和检查。如果这些不变量中的任何一个被违反,就会触发陷阱,导致重新编译或解释。如果执行模式发生变化,即使存在先前的编译结果,也可能会因此触发重新编译。如果由于上述原因从方法缓存中删除了编译结果,则可能会为受影响的方法再次触发编译器。
我在 Windows 上使用 AdoptJDk 11.0.7 Java 并启用了 -XX:+PrintCompilation 标志,所以我可以看到哪些方法是正在编译而不只是解释
我正在我的应用程序中调用一些功能(处理音频文件并创建文件的 html 报告)。我启动应用程序一次(GUI 有限),然后 运行 多次对同一组文件执行相同的任务。第二次调用它 运行s 比第一次快得多,第三次比第二次快一点,随后的 运行s 之间没有太大区别。但我注意到每个 运行 它仍在编译许多方法,并且很多方法都变得不可重入。
是分层编译,所以我明白同一个方法可以重新编译到更高的层次,但是编译的方法数量似乎没有太大变化。
我不明白为什么这么多方法变成不可重入(然后僵尸),我还没有做详细的分析,但似乎是相同的方法一遍又一遍地编译,为什么会这样?
我添加了 -XX:-BackgroundCompilation
选项来强制按顺序编译方法,并让代码等待编译版本,而不是在编译时使用解释版本。这似乎减少了 重入 方法的数量,所以这可能是因为它减少了多个线程尝试访问正在(重新)编译的方法的机会?
但似乎仍有许多方法需要重新编译
例如,在这里我可以看到它被编译到第 3 级,然后被编译到第 4 级,因此第 3 级编译成为非进入者和僵尸。但是随后第 4 级变得不可重入,它又回到第 4 级进行编译,依此类推。
简短的回答是,JIT 反优化会导致已编译的代码被禁用 ("made not entrant")、释放 ("made zombie"),并在再次调用时重新编译(足够次数)。
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
输出中的编译级别范围从 0
到 4
。 0
代表解释,1
到3
代表客户端编译器的不同优化级别,4
代表服务端编译器。在您的输出中,您可以看到 java.lang.String.equals()
从 3
过渡到 4
。发生这种情况时,原始方法被标记为 not_entrant
。它仍然可以被调用,但一旦不再被引用,它将转换为 zombie
。
JVM 清理器 (hotspot/share/runtime/sweeper.cpp
),一个后台任务,负责管理方法生命周期并将 not_reentrant
个方法标记为 zombie
s。清除间隔取决于多种因素,其中之一是方法缓存的可用容量。低容量会增加背景扫描的次数。您可以使用 -XX:+PrintMethodFlushing
(仅限 JVM 调试版本)监视扫描 activity。可以通过最小化缓存大小并最大化其攻击性阈值来增加扫描频率:
-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_entrant
和 zombie
。之后C2的结果也是如此,然后重新编译。
所有先前结果达到 zombie
状态会触发新的编译,即使该方法之前已成功编译。所以,这可能会一遍又一遍地发生。 zombie
状态可能会延迟(如您的情况),具体取决于编译代码的年龄(通过 -XX:MinPassesBeforeFlush
控制)、方法缓存的大小和可用容量以及 not_entrant
方法,命名主要因素。
现在,我们知道这种持续的重新编译很容易发生,就像您的示例中那样 (in_use
-> not_entrant
-> zombie
-> in_use
).但是除了从C1过渡到C2,方法年龄限制和方法缓存大小限制之外,还有什么可以触发not_entrant
以及如何可视化推理?
使用 -XX:+TraceDeoptimization
(仅限 JVM 调试构建),您可以了解给定方法被标记为 not_entrant
的原因。在上面的示例中,输出为(shortened/reformatted 为了便于阅读):
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
实例开始。经过 1 亿次比较后,它将 b
设置为 null
并继续。这就是当时发生的事情(shortened/reformatted 为了便于阅读):
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)
根据统计信息,编译器确定可以消除 java.lang.String.equals()
(if (anObject instanceof String) {
) 使用的 instanceof
中的空检查,因为 b
永远不会为空。经过 1 亿次操作后,不变量被违反,陷阱被触发,导致重新编译并进行空检查。
我们可以扭转局面,通过从 null
开始并在 1 亿次迭代后分配 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 亿次运算后,违反了该不变量并触发了陷阱,导致 recompilation/interpretation 没有分支消除。
编译器执行的优化基于代码执行期间收集的统计信息。优化器的假设通过陷阱记录和检查。如果这些不变量中的任何一个被违反,就会触发陷阱,导致重新编译或解释。如果执行模式发生变化,即使存在先前的编译结果,也可能会因此触发重新编译。如果由于上述原因从方法缓存中删除了编译结果,则可能会为受影响的方法再次触发编译器。