使用 Eclipse OpenJ9 優化 JVM 啟動
前言
應用程式啟動時間對各類人群都很重要,其中原因也不盡相同。例如,反覆執行編碼-編譯-測試周期的軟體開發人員非常關注啟動時間,因為他們希望到達更快地執行新程式碼的時間點。在軟硬體升級或故障導致計劃中或計劃外的執行中斷後,需要快速恢復,這時啟動時間也很重要。在雲中,如果自動擴充套件系統需要快速啟動應用程式的其他例項來處理負載的臨時增長,那麼啟動時間也至關重要。
在 Eclipse OpenJ9 中,JVM 啟動時間被視為首要的效能指標。為此,OpenJ9 修改了啟動階段的內部啟發法,以便改善應用程式的啟動時間。那麼,OpenJ9 如何檢測啟動階段呢?儘管具體細節不屬於本文的討論範圍,但可以認為 OpenJ9 的階段檢測機制基於以下觀察:啟動的典型特徵是密集的類載入和位元組碼解釋,並伴隨持續的 JIT 編譯活動。
由於解釋是一個非常耗費資源的過程(解釋程式碼通常比原生程式碼慢 10 倍),所以 OpenJ9 JIT 編譯器的目標是儘快減少解釋。因此,JIT 在此階段的目標是儘快編譯儘可能多的 Java 方法,而不過度關心生成的程式碼質量。為此,在啟動期間,JIT 編譯器可以選擇:
- 降低方法的優化級別。這些低優化方法的主體可以通過各種機制進行升級,其中最重要的機制是守護計數重新編譯 (guarded-counting-recompilation, GCR)。GCR 是一種基於呼叫計數的重新編譯機制,但因為會在啟動階段禁用計數,所以它是"被守護的"。
- 減少解釋方法的呼叫計數。在 OpenJ9 中,編譯解釋方法的決定基於呼叫計數機制。雖然從啟動角度來看,使用低呼叫閾值可能很吸引人,但這會導致在解釋方法時收集的分析資料質量較差,而對於優化器而言,這些分析資料是實現良好吞吐量所必需的。因此,OpenJ9 僅會在啟動階段使用減少的呼叫計數。
- 優先處理首次編譯請求(將一個方法從解釋型轉變為原生程式碼)和消耗資源更少的編譯。保持未完成編譯請求的佇列將作為優先佇列來實現,而且 OpenJ9 偏愛那些提供了最佳"價值"的編譯。本著這種思路,將具有較高優化級別的重新編譯和編譯放在佇列末尾。
OpenJ9 中旨在改善啟動時間的兩種主要機制是共享類快取 (SCC) 和動態提前 (AOT) 編譯技術。這些技術直接解決了主要的開銷來源。
SCC 是一個記憶體對映檔案,主要儲存 3 種資料:
- ROMClasses
- AOT 生成的程式碼
- 直譯器分析器資訊
在 OpenJ9 中,Java .class 檔案首先被轉換為一個名為 ROMClass 的內部表示,其中包含該類的所有不可變資料。從 SCC 載入 ROMClass 要快得多,因為:
- 類的資料是從記憶體而不是從磁碟抓取的
- 向 ROMClass 的轉換和一些驗證已經發生
但是注意,並非所有類都可以儲存在 SCC 中。主要條件是,用於執行載入的類載入器是 SCC "感知的"。滿足此前提條件的最簡單方法是,擁有一個擴充套件 java.net.URLClassLoader
的類載入器。要進一步瞭解 SCC 技術,請查閱 " Eclipse OpenJ9 中的類共享 "(IBM Developer,2018 年 7 月)。
動態 AOT 編譯是這樣一種機制:在某個 JVM 呼叫中編譯的 Java 方法會儲存在 SCC 中,並會在後續 JVM 呼叫中重用。與執行一次 JIT 編譯相比,從 SCC 載入經過 AOT 編譯的主體要快得多,而且消耗的資源要少得多。啟動時間之所以得到明顯改善,原因有兩個:
- 編譯開銷顯著減少(因此 JIT 編譯執行緒從應用程式執行緒中竊取的 CPU 週期更少)
- 方法能夠更快地從解釋狀態過渡到編譯狀態
要注意的一點是,由於需要在 JVM 呼叫之間共享經過 AOT 編譯的主體而造成的技術限制,AOT 編譯的程式碼質量略低於 JIT 編譯的質量。OpenJ9 通過以下方法克服了這個缺點:
- 重新編譯頻繁執行的 AOT 主體
- 將 AOT 生成限制在應用程式的啟動階段
OpenJ9 中的直譯器分析器機制收集關於分支偏好(接受或不接受)的分析資料,以及介面呼叫、虛擬呼叫、 checkcast
操作和 instanceof
操作的目標。此資訊對 JIT 優化器至關重要,但不幸的是,分析資料收集過程的開銷較高,這會對啟動時間產生負面影響。OpenJ9 採用的解決方案是將收集的分析資料儲存在 SCC 中,並在後續執行中使用,同時在後續執行的啟動期間關閉直譯器分析器。一旦對分析資料的查詢太多而一無所獲,那麼監視(watchdog)機制可能會開啟直譯器分析器。
使用者應如何改善啟動時間?
儘管 OpenJ9 中預設啟用了許多面向啟動的啟發法,但在某些情況下需要使用者輸入資料。
配置並調優 SCC/AOT
在編寫本文時,預設情況下未啟用 SCC 和動態 AOT。使用者需要指定 -Xshareclasses
命令列選項。請參閱 GitHub 上的 -Xshareclasses
文件 瞭解完整的子選項列表。常見的陷阱之一是,對於已連線的應用程式,SCC 的預設大小可能太小。可通過使用以下命令輸出 SCC 統計資料並檢視 SCC 佔用情況來確定此情形:
java -Xshareclasses:name=YourSCCName,printStats
如果輸出顯示為 " Cache is 100% full
",那麼使用較大的 SCC 可能會讓應用程式受益。在過去的 OpenJ9 版本中,為了通過 -Xscmx
選項增加 SCC 的大小,必須銷燬現有的 SCC 並建立新的 SCC。從 OpenJ9 v0.9.0 開始,此過程已簡化,因為 SCC 定義了一個軟性限制和一個硬性限制。當達到軟性限制時,SCC 會宣告已裝滿,但可以使用 -Xshareclasses:adjustsoftmx=<size>
選項將此大小增加到硬性限制,而不銷燬 SCC。
請參閱 GitHub 上的 -Xscmx
文件 瞭解更多調節 SCC 大小的細節。
對於 AOT 程式碼大小,OpenJ9 預設情況下不會設定任何顯式限制,這意味著 AOT 程式碼可以儲存在 SCC 中,直到它被裝滿。但是,您必須注意,某些應用程式會使用 -Xscmaxaot<size>
選項在內部設定一個 AOT 空間限制。一些典型的示例包括 WebSphere Application Server 和 WebSphere Liberty。如果在這些應用伺服器上執行的應用程式特別大,使用者可以考慮增加 AOT 空間限制,以便進一步改善啟動時間。可使用 printStats
選項獲取有關 AOT 空間限制和佔用情況的統計資料(參閱上面的示例)。
使用 -Xtune:virtualized
對於 CPU 資源受限的環境,比如通常在雲中構建的環境,建議使用此選項。在內部,該選項通過其內聯和重新編譯決策來讓 JIT 編譯器變得更加保守(從而節省 CPU 資源),而 GC 模組不那麼熱衷於擴充套件堆(從而降低記憶體佔用)。這些更改預計會將 JIT 編譯執行緒耗用的 CPU 資源減少 20-30%,將記憶體佔用提高 3-5%,而代價是較小的 (2-3%) 吞吐量損失。此選項本身對啟動時間的影響極小,但與 AOT 結合使用時,可以很好地改善啟動時間。原因有兩個:
-
-Xtune:virtualized
會在內部啟用-Xaot:forceaot
選項,該選項指示 JIT 編譯器繞過其常用的啟發法,生成儘可能多的 AOT 程式碼 - 所有 AOT 編譯的優化級別從"冷"(通常在啟動期間使用)提升到"暖"
但是,提醒一句:儘管 -Xtune:virtualized
與較大的 SCC 結合可以很好地改善應用程式的啟動和載入時間,但吞吐量可能會受到影響,如先前所述,AOT 程式碼質量與 JIT 程式碼質量不匹配,重新編譯機制明顯減弱,導致許多 AOT 主體無法重新編譯。
使用 -Xquickstart
在以下情況下推薦使用此選項:
- 在非常短的應用程式中,沒有足夠的時間來緩衝 JIT 編譯的成本
- 在圖形/互動式應用程式中,來自 JIT 編譯活動的干擾可能讓人感到不穩定
- 當用戶認為啟動時間是最重要的效能指標時
如您所料, -Xquickstart
模式下的更改旨在實現快速啟動體驗。JIT 將完全禁用直譯器分析器,將所有首次編譯降級為"冷"優化級別,減少方法呼叫計數,並開啟 -Xaot:forceaot
模式(如果適用)。注意,雖然禁用直譯器分析器從啟動角度來看是有利的,但會導致吞吐水平降低(在使用 -Xquickstart
時,經常可以看到吞吐量下降達 40%)。
影響啟動時間的其他設定
對於希望實現進一步改進的使用者,還有一個選項可以進一步減少啟動時間,儘管減少幅度有限。OpenJ9 中的 Java 堆的預設初始大小為 8MB,通過 -Xms<size>
增加此值可以降低 GC 開銷,從而改善啟動時間。但是,缺點是記憶體消耗略有增加。例如,在我們的 Liberty+DT7 試驗中,我們看到將 -Xms256M
新增到命令列後,啟動時間減少了 6%,但記憶體佔用增加了 8%。
試驗結果
面對如此多的選擇,您可能會問應該挑選哪一個。為了回答這個問題,我們對 WebSphere Liberty 18.0.0.2 上安裝的 Daytrader7 基準應用程式 運行了一些啟動試驗,並使用了從 AdoptOpenJDK 下載的帶 OpenJ9 build 的 OpenJDK8 OpenJDK8U_x64_linux_openj9_linuxXL_2018-09-27-08-47。
圖 1. 不同 OpenJ9 選項之間的啟動時間對比
如圖 1 所示,在 -Xshareclasses
與 -Xquickstart
之間,應優先考慮前者,因為它提供了更大的啟動時間改進而對吞吐量的影響非常小(如果有的話)。但在某些情況下,SCC 無法實現其預定目標,例如,由於許多 Java 類是通過一個無法感知 SCC 的類載入器載入的。在這些情況下,可以考慮使用 -Xquickstart
作為替代。
圖 2. SCC/AOT 大小對啟動時間的影響
需要注意的是, -Xquickstart
可以與 SCC/AOT 結合使用,而且這種組合可以比任何單個元件更大地改進啟動時間。如圖 2 所示,如果將 -Xquickstart
新增到 WebSphere Liberty 所採用的預設 SCC/AOT 設定 (SCC=60MB/AOT=8MB) 中,可以將啟動時間再提高 7%。相比之下,新增 -Xtune:virtualized
似乎會使應用程式的啟動時間延長了 2%。但是,這只是 WebSphere Liberty 中的小 SCC 和 AOT 空間的一個工件:因為在 -Xtune:virtualized
下,AOT 程式碼在"暖"優化級別(而不是"冷")下編譯,編譯後的主體更加龐大,超出了 SCC 的容量。如果將 SCC 和 AOT 分別增大到 75MB 和 20MB,我們就可以看到 -Xtune:virtualized
的真正潛力,與預設 Liberty 設定相比,這可以將啟動時間縮短 13%。
總之,如果啟動時間對您至關重要,那麼結合使用 -Xshareclasses
和 -Xtune:virtualized
應該是首先嚐試的配置之一。您僅需確保為您的 SCC 和 AOT 空間設定了適當的大小。