![](/img/trans.png)
[英]Why does the Java compiler 11 use invokevirtual to call private methods?
[英]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 有限),然后在同一組文件上多次運行相同的任務。 第二次調用它的運行速度明顯快於第一次,第三次比第二次稍快,然后后續運行之間沒有太大差異。 但我注意到每次運行時它仍在編譯許多方法,並且許多方法變得不可重入。
它是分層編譯,所以我知道可以將相同的方法重新編譯到更高級別,但是正在編譯的方法數量似乎沒有太大變化。
我不明白為什么這么多方法變得不可重入(然后是僵屍) ,我還沒有做詳細的分析,但似乎相同的方法正在一遍又一遍地編譯,為什么會這樣?
我添加了-XX:-BackgroundCompilation
選項來強制按順序編譯方法,並讓代碼等待編譯版本,而不是在編譯時使用解釋版本。 這似乎減少了可重入方法的數量,所以這可能是因為它減少了多個線程嘗試訪問正在(重新)編譯的方法的機會?
但仍有許多方法似乎被重新編譯
例如,在這里我可以看到它被編譯到第 3 級,然后它被編譯到第 4 級,所以第 3 級編譯是非進入的並且是僵屍的。 但隨后第 4 級變得不可重入,它又回到第 4 級編譯,依此類推。
簡短的回答是,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
)禁用但仍然可以調用,或者如果它是non_entrant
並且不再使用,它可以被標記為zombie
。 最后,可以將該方法標記為卸載。
在分層編譯的情況下,客戶端編譯器 (C1) 生成的初始編譯結果可能會替換為服務器編譯器 (C2) 的編譯結果,具體取決於使用統計信息。
-XX:+PrintCompilation
output 中的編譯級別范圍為0
到4
。 0
代表解釋, 1
到3
代表客戶端編譯器的不同優化級別, 4
代表服務器編譯器。 在您的 output 中,您可以看到java.lang.String.equals()
從3
過渡到4
。 發生這種情況時,原始方法被標記為not_entrant
。 它仍然可以被調用,但一旦不再被引用,它就會轉變為zombie
。
JVM 清掃器( hotspot/share/runtime/sweeper.cpp
),后台任務,負責管理方法生命周期,將not_reentrant
方法標記為zombie
。 掃描間隔取決於許多因素,其中一個是方法緩存的可用容量。 低容量會增加背景掃描的次數。 您可以使用-XX:+PrintMethodFlushing
(僅限 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 調試版本)以強制立即轉換。
下面的代碼將觸發以下 output:
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
state 以獲取所有先前的結果會觸發新的編譯,即使該方法之前已成功編譯。 所以,這可能會一次又一次地發生。 zombie
state 可能會延遲(如您的情況),具體取決於編譯代碼的年齡(通過-XX:MinPassesBeforeFlush
控制)、方法緩存的大小和可用容量以及not_entrant
方法的使用,以命名主要因素。
現在,我們知道這種持續的重新編譯很容易發生,就像在您的示例中那樣( in_use
-> not_entrant
-> zombie
-> in_use
)。 但是除了從 C1 到 C2 的轉換、方法年齡限制和方法緩存大小限制之外,還有什么可以觸發not_entrant
以及如何可視化推理?
使用-XX:+TraceDeoptimization
(僅限 JVM 調試構建),您可以了解給定方法被標記為not_entrant
的原因。 在上面的例子中,output 是(為了可讀性而縮短/重新格式化):
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)
根據統計,編譯器確定java.lang.String.equals()
( if (anObject instanceof String) {
) 使用的instanceof
中的 null 檢查可以被消除,因為b
從來沒有 Z37A6259CC0461DAE298DFF0BDZ8DFF0BZ866 經過 1 億次操作后,該不變量被違反並觸發了陷阱,導致使用 null 檢查重新編譯。
我們可以通過從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
始終是 null。 可以消除包括條件在內的整個代碼塊。 在 1 億次操作之后,違反了該不變量並觸發了陷阱,導致重新編譯/解釋沒有分支消除。
編譯器執行的優化基於代碼執行期間收集的統計信息。 通過陷阱記錄和檢查優化器的假設。 如果違反了這些不變量中的任何一個,則會觸發陷阱,導致重新編譯或解釋。 如果執行模式發生變化,即使存在先前的編譯結果,也可能會觸發重新編譯。 如果由於上述原因從方法緩存中刪除了編譯結果,則可能會為受影響的方法再次觸發編譯器。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.