隨著 2023 年秋季 JDK 21 的發布,現在有乙個新的 LTS 版本來基準測試和生成一些 GC 效能圖表。 JDK 21 和 JDK 17 之後的其他版本提供了一系列值得注意的功能,例如虛擬執行緒、Switch 模式匹配和分代 ZGC。 讓我們看看它的表現如何。
介紹
在比較不同 JDK 版本之間的效能時,可能很難確定哪些功能提供了一定的效能提公升。 但很容易看出,自 JDK 8 以來,整個 J**a 平台的效能有了顯著的提高。 在本文中,我將使用 specjbb 20151 來展示效能改進。 這是乙個眾所周知的標準基準,非常適合演示對 GC 的改進。 這主要是因為基準測試提供了兩個分數:
max-jops:原始吞吐量critical-jops:延遲約束下吞吐量GC的提高會提高兩個分數,但延遲約束下分數的提高與GC的變化關係更密切。 基本上,較短的超時將獲得更好的分數。 對 JIT 和 J**a 平台其他部分的改進也會在原始吞吐量分數中發揮作用。
在基準測試時,我沒有做太多調整,但我設定了 16 GB 的固定堆大小,並啟用了大頁面,並確保在執行基準測試之前對它們進行分頁。 我希望結果能夠反映即插即用的行為,但這樣的配置會給你帶來公平和一致的結果。
選擇 GC
Oracle 支援 4 種不同的 GC,它們都有不同的用途。 在這篇文章中,我不包括序列 GC,因為它不適用於我正在使用的基準測試。 序列 GC 的主要重點是低開銷,主要適用於記憶體和 CPU 資源有限的用例。 在這次比較中,我們將重點關注以下 GC:
g1:自 JDK 9 以來的預設收集器,重點關注延遲和吞吐量之間的平衡parallel:以吞吐量為導向的收集器,可能會遇到較長的最壞情況延遲z:超低延遲、完全併發、低於毫秒級的暫停時間的替代方案,以及使用哪種 GC 取決於應用程式最關心的問題。 每個 GC 都有最佳替代方案,並且沒有乙個 GC 在所有用例中都表現最佳。
進展
如果你回顧一下自 JDK 8 以來所取得的進步,就會發現 G1 和 Parallel 在各個方面都取得了驚人的進步。 兩個收集器在各個方面都得到了改進。 它們暫停的時間更少,使用的記憶體更少,並且具有比以往更好的吞吐量。 ZGC 並沒有那麼久,在這篇文章中,我主要關注使 ZGC 成為世代收集器所帶來的改進。
本文中的圖表分別比較了不同的收集器。 主要原因是,根據堆大小的配置,結果對某個收集器更有利。 通過這樣做,我們可以專注於所有 GC 的巨大進步,而不是試圖提供最好的 GC。
比較包括 JDK 8、JDK 17 和 JDK 21 的 G1 和 Parallel。 對於 ZGC,我選擇的三個資料點是 JDK 17、JDK 21 和 JDK 21 中的分代 ZGC。 由於 JDK 17 是第乙個完全受 ZGC 支援的 LTS 版本,因此回顧早期版本沒有多大意義。
吞吐量
在原始吞吐量效能方面,自 JDK 17 以來的改進並不大,但仍有輕微的提公升。 但是,在下面的圖表中,有兩件事確實值得關注。 首先,JDK 8 和最新的 JDK 之間的顯著區別在於 G1 和 Parallel。 從效能的角度來看,離開 JDK 8 從未像現在這樣有益。
第二個值得注意的注意事項是使用分代 ZGC 時看到的 10% 提公升。 ZGC 中的新一代支援使其在記憶體中更加高效,而不必考慮整個堆的每個 GC。 其效果是,在執行 GC 工作時消耗的 CPU 資源更少,可用於應用程式,從而提高其效能。
延遲
對於延遲評分,情況基本相同。 G1 和 Parallel 在 JDK 8 和 JDK 17 之間取得了巨大的進步,但最好的結果仍然是在 JDK 21 中。 值得注意的是,JDK 17 和 JDK 7 之間有 8 年多的創新時間,而 JDK 17 和 JDK 21 之間只有兩年的創新時間。 相比之下,指導性案例的短時間框架和成熟度使得在如此大的基準上取得了重大進展。
將幾代人引入 ZGC 並仍然看到世代 ZGC 與傳統模型之間的顯著差異,這真是太好了。 值得注意的是,大部分改進來自吞吐量分數的增加。 這兩種 ZGC 模式之間的暫停時間沒有太大差異,兩者都遠低於 1 毫秒。 但是,當考慮到最壞情況的延遲時,與傳統模式相比,分代 ZGC 要好一些。
對於 G1 和 Parallel,暫停時間沒有太大變化。 我們在 g1 上花了更多的時間,在這裡,通過檢視更高的百分位數停頓,我們可以看到我們已經設法將時間縮短了幾毫秒。
記憶體開銷
最後乙個圖表比較了在固定負載下執行基準測試時的峰值本機記憶體開銷。 從這個角度來看,Parallel 非常穩定,我們沒有花任何時間進一步優化它。 另一方面,在G1中,我們在過去十年中設法消除了許多低效率的問題,在JDK 20中,我們將G1更改為只需要乙個標記位圖,而不是兩個。 由此節省的資源非常顯著,G1 現在是該基準測試中記憶體效率最高的收集器。
對於分代 ZGC,我們可以清楚地看到為了在此基準測試中獲得更好的延遲和吞吐量而做出的權衡。 權衡是更高的本機記憶體消耗。 為了有效地實現代際支援,我們需要跟蹤從老一代到新一代的指標。 這些稱為記憶體集,它們占用記憶體。 當處理多代時,我們還需要一些記憶體來儲存其他元資料。 儘管如此,在大多數情況下,與傳統 ZGC 相比,使用分代 ZGC 時,總體記憶體消耗會更低,因為它不需要處理給定工作負載所需的堆大小。 因此,通常可以通過使用較小的堆來節省額外的本機記憶體使用量,並且仍然可以獲得更好的整體效能。
公升級並嘗試分離 ZGC 的代次
如上所述,與 JDK 8 相比,JDK 21 的效能得到了顯著提高。 因此,如果您仍在使用 JDK 8,您應該開始考慮公升級。 這也是在公升級時重新評估要同時使用的 GC 的好時機。 如果遷移到 JDK 21,我強烈建議嘗試分代 zgc。 在 JDK 21 中,ZGC 在分代版本和舊版本中都可用,要使用分代版本,您需要指定以下兩個選項:
-xx:+usezgc -xx:+zgenerational
**: Stefan Johansson, link: Kstefanjgithub.io/2023/12/13/jdk-21-the-gcs-keep-getting-better.html