簡體   English   中英

為什么 Java 即時編譯器繼續重新編譯相同的方法並使方法不可租用

[英]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 中的編譯級別范圍為04 0代表解釋, 13代表客戶端編譯器的不同優化級別, 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_entrantzombie 稍后,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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM