JDK 18 G1/Parallel/Serial GC changes

原文はこちら。
The original article was written by Thomas Schatzl (OpenJDK developer, Oracle).
https://tschatzl.github.io/2022/03/14/jdk18-g1-parallel-gc-changes.html

あっという間に時間が過ぎ、JDK18のGAが間近に迫ってきました(訳注:この記事の発表日は2022/3/14です)。このリリースにおけるHotspotのStop-the-world GC、G1とParallel GCの変更点、特に改善点については別の機会にまとめたいと思います。

JDK 18
https://openjdk.java.net/projects/jdk/18/

このリリースでは、GC分野に特化したJEPはありませんが、GC全般に大きな影響を与えるものがあり、それがJEP 421: Deprecate Finalization for Removalです。

JEP 421: Deprecate Finalization for Removal
https://openjdk.java.net/jeps/421

ファイナライザの存在は、コードが非常に複雑になるのに加えて、GCにいくつかのパフォーマンスにネガティブなインパクトを及ぼします。GCエンジニアとして個人的には、ファイナライザを取り除くことができるようになることを楽しみにしています。信頼性が高く安全な方法で使用することはほとんど不可能であるとJEPで指摘されている通り、ファイナライザは、使用上の欠点が非常に多数あります。

このJEPに関連した特別なタイムラインはありません。そして著者は「… finalizationを取り除くまでに長い移行期間を見込んでいる …」と述べているので、少なくともしばらくの間はfinalizationは残るでしょう。

Hotspot GCサブコンポーネント全体の変更点一覧はこちらから確認いただけます。合計300の変更点があります。驚くようなことはありません。

ZGCに目を向けると、このリリースでの変更は比較的小さなものであることがわかります。

ZGC
https://wiki.openjdk.java.net/display/zgc/Main

JDK-8267186では文字列の重複排除と、PPC64ポート(JDK-8274851)が追加されました。また、いくつかの使い勝手の改善とバグフィックスもありました。

[JDK-8267186] Add string deduplication support to ZGC
https://bugs.openjdk.java.net/browse/JDK-8267186
[JDK-8274851] [ppc64] Port zgc to linux on ppc64le
https://bugs.openjdk.java.net/browse/JDK-8274851

しかし、generational ZGC(短命オブジェクトと長命オブジェクトを個別にGCする仕組みを持つZGC)は絶賛開発中です。開発の状況は以下からフォローできますので、楽しみにしていてください。

ZGC generational
https://github.com/openjdk/zgc/tree/zgc_generational

Generic improvements

JEP 192で説明されているように、すべての OpenJDK GCが文字列の重複排除をサポートするようになりました。

JEP 192: String Deduplication in G1
https://openjdk.java.net/jeps/192

Parallel GCはJDK-8267185で、Serial GCはJDK-8272609で、そしてZGCはJDK-8267186でサポートを実装しました。

[JDK-8267185] Add string deduplication support to ParallelGC
https://bugs.openjdk.java.net/browse/JDK-8267185
[JDK-8272609] Add string deduplication support to SerialGC
https://bugs.openjdk.java.net/browse/JDK-8272609
[JDK-8267186] Add string deduplication support to ZGC
https://bugs.openjdk.java.net/browse/JDK-8267186

JEPで説明されているものとは実装が異なるものの、基本的な原理はそのまま適用されています。
-XX:+UseStringDeduplicationオプションを使用して有効化します。

V. Chandが、カードテーブルのカードサイズを設定できるようにする変更を提供してくれました。
-XX:GCCardSizeInBytesを使ってこの値を変更できます。12825651210241024は64ビットのみ)の値が許可されています。

[JDK-8272773] Configurable card table card size
https://bugs.openjdk.java.net/browse/JDK-8272773

この値を変更することで、GCポーズ時間を改善できます。これは興味深いトピックなので、より詳細な説明を含む以下のブログ記事を書いています。

Card Table Card Size Shenanigans
https://tschatzl.github.io/2022/02/15/card-table-card-size.html
https://logico-jp.io/2022/03/16/card-table-card-size-shenanigans/

Serial GC

Serial GCでは、JDK-8273508でアーカイブ済みヒープオブジェクトのサポートを追加しています。これにより起動時間の顕著な短縮が可能になっています。使い方はG1の場合と同じです。

[JDK-8273508] Support archived heap objects in SerialGC
https://bugs.openjdk.java.net/browse/JDK-8273508

G1 GC

今回のリリースでは、ユーザーから見える、G1 に特化した変更がかなりあります。

記憶集合 (remembered set) の書き換えにより、コストをかけずにG1のネイティブメモリ利用量を大幅に削減します。これはJDK-8017163で取り扱っているもので、記憶集合のネイティブ・メモリーのフットプリントを大幅に削減します。

[JDK-8017163] G1: Refactor remembered sets
https://bugs.openjdk.java.net/browse/JDK-8017163
High Update RS and Scan RS Times
https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-garbage-collector-tuning.html#GUID-A0343B53-A690-4DDE-98F9-9877096DBF0F
高いRSの更新およびRSのスキャン時間
https://docs.oracle.com/javase/jp/17/gctuning/garbage-first-garbage-collector-tuning.html#GUID-A0343B53-A690-4DDE-98F9-9877096DBF0F

下図は、あるオブジェクトキャッシュのようなアプリケーションのGCコンポーネントについて、JDK 17とJDK 18のネイティブメモリ消費量を比較したものです。Native Memory Tracking機能でレポートを取得しています。

Native Memory Tracking
https://docs.oracle.com/en/java/javase/17/vm/native-memory-tracking.html
https://docs.oracle.com/javase/jp/17/vm/native-memory-tracking.html

Memory usage BigRAMTester 20GB

ネイティブメモリ使用量、特に記憶集合のメモリ使用量は、安定状態でピーク時の約2GB(JDK 17、青)から1.3GB(JDK 18、紫)に減少し、約35~40%のメモリを節約できています。

トリビア:同じアプリケーションでJDK 8はほぼ5.8GB、JDK 11は4GBのネイティブメモリーを使用します。

この変更により、GCCardSetという新しい NMTカテゴリが追加され、G1メモリーセットのみのネイティブメモリ使用量がカウントされるようになりました。既存のGCというNMTカテゴリには、その他のすべてのガベージコレクションネイティブメモリ使用量が含まれています。上図では、JDK 18のGCコンポーネントのみのネイティブ・メモリ使用量が黄色の一番下の線で表示されています。

以下は、これらのカテゴリが NMT レポートでの見え方を示す例です。

-                        GC (reserved=883951KB, committed=883951KB)
                            (malloc=72291KB #10978)
                            (mmap: reserved=811660KB, committed=811660KB)
                        
-                 GCCardSet (reserved=241444KB, committed=241444KB)
                            (malloc=241444KB #36143)

このように分離することで、必要に応じて記憶集合の構成をチューニングできます。

JDK 18では、JDK-8275056の変更により、一部の内部データ構造で課されていた32MBというリージョンサイズ制限を解除しました。現時点では、ヒープのリージョン最大サイズは512MBに設定されていますが、もっと大きくすることも可能です。詳しくは、このトピックに関する以前のブログ記事をご覧ください。

[JDK-8275056] Virtualize G1CardSet containers over heap region
https://bugs.openjdk.java.net/browse/JDK-8275056
Heap Regions X-Large
https://tschatzl.github.io/2021/11/15/heap-regions-x-large.html
https://logico-jp.io/2021/11/30/heap-regions-x-large/

Hamlin Li による G1でのregion pinningのためのビルディングブロックとしての退避失敗の改善については、さらに進展がありました。最近、対応するJEP 423がcandidateになりました。このJEPには、この変更の目的について多くの情報が記載されています。

JEP 423: Region Pinning for G1
https://openjdk.java.net/jeps/423

Pause times with induced evacuation failures

上図は、JDK 17上で、固定注入率(負荷一定)で実行されたSPECjbb2015のGCポーズ時間を示したものです(青色)。予想通り、ポーズ時間は基本的に平坦です。紫色の線は、JDK 17で定期的に発生する退避失敗(evacuation failures、約5回ごとのGC)を伴うポーズ時間を示していますが、ポーズ時間が大幅に増加しています。JDK 18の場合、黄線で示すように改善されています。JDK 18の場合、ポーズ時間は退避失敗のない、つまり固定注入率(負荷一定)の場合のポーズ時間よりもさらに短くなります。

JDK 17の動作よりはるかに良いものの、これはもちろん最適とは言えません。というのも、G1が必要以上に(短時間ではありますが)GCを行うことを意味しているからです。エルゴノミクス的には、以下の記事末尾のIssueリストにあるように、old世代に昇格した追加リージョンにうまく対処できません。

Evacuation Failure and Object Pinning
https://tschatzl.github.io/2021/06/28/evacuation-failure.html
https://logico-jp.io/2021/09/26/evacuation-failure-and-object-pinning/

これを修正するための改善例として、JDK-8254739があります。

G1: Optimize evacuation failure for regions with few failed objects
https://bugs.openjdk.java.net/browse/JDK-8254739

G1は、Javaアプリケーションが終了しようとしているときに、アクティブなconcurrent markingの完了を待って、実際に終了するケースがありました。これは、そのタスクの複雑さに応じて非常に長い遅延を引き起こす可能性があります。JDK 18では、JDK-8273605でこの問題を修正し、VMを「直ちに」終了させるようにしました。

[JDK-8273605] VM Exit does not abort concurrent mark
https://bugs.openjdk.java.net/browse/JDK-8273605

JDK-8278824は、JDK 14 (JDK-8213108) で導入された興味深いパフォーマンス劣化を修正します。

[JDK-8278824] Uneven work distribution when scanning heap roots in G1
https://bugs.openjdk.java.net/browse/JDK-8278824
[JDK-8213108] Improve work distribution during remembered set scan
https://bugs.openjdk.java.net/browse/JDK-8213108

GC対象のリージョンへの参照を探す際、最初は退避作業がうまく分配されません。大きなマシンでは、これはwork-stealingメカニズムの問題を露呈し、過度に長いポーズ時間をもたらすことになります。

Repro pause times

上図はこの問題を示しています。JDK 11(青)の場合、ポーズ時間は測定期間を通じてかなり安定しています。これとは対照的に、JDK 17(修正なし、紫)では、最初はかなり速いのですが、330秒あたりでポーズ時間に大きなスパイクが発生しています。このタイミングで、いくつかの大きなj.l.Object配列が、そのサイズゆえにold世代に入れられています。この時点で、GCはこの配列を毎回スキャンする必要があります。最初の作業分配がうまくいかず、work stealingが十分に機能しないため、多くのコアプロセッサーでポーズ時間が増加します。

W. Kemperが提供してくれた修正により、初期の作業分配が改善され、ポーズ時間が正常に戻りました。JDK 18の結果は黄色の線で示されています。この変更はすでに最新のJDK 17にバックポートされていますので、最新であることを確認してください。

ガベージコレクションチューニングガイドを更新し、JDK 18のG1の現在の状態を反映し、さまざまなセクションを改善しました。一般的なセクションでいくつかの細かい説明も入っています。JDK 18のガベージコレクションチューニングガイドは以下からどうぞ。

HotSpot Virtual Machine Garbage Collection Tuning Guide
https://docs.oracle.com/en/java/javase/18/gctuning/index.html

What’s next

もちろん、とっくにJDK19の開発に着手しています。ここでは、現在開発中で、注目される可能性のある興味深い変更点を簡単にリストアップしていきます。保証はありませんが、いつものように、完了したら統合される予定です。

G1 (JDK-8280396) と Parallel GC (JDK-8280705) のFull GCで、ばらつきの大きい長時間のポーズを引き起こすという、作業配分の潜在的なスターベーション (starvation) 問題は、すでに統合済みです。

[JDK-8280396] G1: Full gc mark stack draining should prefer to make work available to other threads
https://bugs.openjdk.java.net/browse/JDK-8280396
[JDK-8280705] Parallel: Full gc mark stack draining should prefer to make work available to other threads
https://bugs.openjdk.java.net/browse/JDK-8280705

JDK-8278824ならびに上記Bugのレビューで指摘されている通り、GC中の作業配分に問題があることが判明しています。work stealing対象の犠牲者の選択と実際のwork stealingは、特に高いスレッド個数でやや非効率的です。

8278824: Increase chunks per region for G1 vm root scan #6840
https://github.com/openjdk/jdk/pull/6840#issuecomment-994936159

Repro pause times

上図は、プロトタイプの結果を水色で追加したものですが、有望な結果を示しています。

H. Liは、G1のregion pinningに関する共同研究を続けています (JEP 423)。

JEP 423: Region Pinning for G1
https://openjdk.java.net/jeps/423

JDK-8210708では、さらにフットプリントを削減する実験を行っています。この変更後のネイティブメモリ消費量は、JDK-8210708が統合された場合、下図の水色の線のようになる可能性があります。

[JDK-8210708] Investigate dropping one of the mark bitmaps in G1
https://bugs.openjdk.java.net/browse/JDK-8210708

Native memory usage BigRAMTester 20GB

このアプリケーションにおいて、マークビットマップを削除することによって節約できるメモリサイズが記憶集合のメモリサイズとほとんど同じなのはたまたまです。通常、Javaアプリケーションの記憶集合はこのケースよりもはるかに小さいので、ネイティブメモリーのフットプリントの相対的な利得ははるかに高くなるはずです。

ランタイム・チームは、JDK-8274788でParallel GC向けのヒープ・オブジェクトのアーカイブのサポートに取り組んでいるところです。

[JDK-8274788] Support archived heap objects in ParallelGC
https://bugs.openjdk.java.net/browse/JDK-8274788

詳細はのちほど。

Thanks go to…

今回も素晴らしいJDKのリリースに貢献くださったみなさま、どうもありがとうございました。次のリリースでお会いしましょう。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中