Amazon Web Services ブログ

Amazon Corretto の Shenandoah GC のジェネレーショナルモード プレビューリリースのお知らせ

(この記事は、2021/11/30 に公開された “ Announcing preview release for the generational mode to the Shenandoah GC ” を翻訳したものです。)

Amazon Corretto チームは、 Shenandoah GC のジェネレーショナルモードを発表できることを嬉しく思います。これは、従来のシングルジェネレーションの Shenandoah にジェネレーショナルモードを追加するという GC への重要な貢献を、 Red Hat と協力して行った結果です。 Java の主な利点の 1 つは、 Java 仮想マシン ( JVM ) がメモリ管理を自動的に処理することです。 JVM によるアプリケーションのスループットとレスポンスタイムへの影響を最小限に抑える努力によって、多くのイノベーションが生まれました。 Shenandoah や ZGC ガベージコレクタ ( GC ) などの最近のメモリマネージャは、最先端の自動メモリ管理の代表的なものです。

どのようなメリットがありますか?

ジェネレーショナルモードを追加することで、 Amazon Corretto チームは、高いメモリアロケーション( 4 GB /秒超 )や高いライブメモリ使用率( 60 %超 )でアプリケーションを構築したい Java 開発者の幅広いユーザーに、 Shenandoah のメリットを提供します。特定のワークロードにおいて、 Shenandoah の新しいジェネレーショナルモードは、3 分の 1 のヒープサイズを使用して、従来の Shenandoah の応答時間に匹敵することができ、最大 GC 一時停止レイテンシーが 10 ミリ秒未満になるように、お客様が設定することができます。従来の Shenandoah と同様のハードウェア構成で、ジェネレーショナルモードは、ハードウェアコストを削減し、アグレッシブな応答時間の SLA を、より高いパーセンタイルの遵守で実現することができます。

このプレビューリリースにおいて、 Shenandoah ジェネレーショナルモードは、Dacapo benchmark suite の一部のベンチマークで改善が実証されました。

  • G1 のメモリ効率と、シングルジェネレーションの Shenandoah の、短い休止時間のギャップを埋めています。(訳者注: G1 とは、ガベージファースト・ガベージ・コレクタを指しています)
  • p99 の一時停止時間を 10 ミリ秒未満に維持し、ヒープ使用率を改善します。(訳者中: p はパーセンタイルを表しています)
  • シングルジェネレーションの Shenandoah と比較して、短命なオブジェクトに対して持続的に高いアロケーションを実現しています。
  • アロケーションの急増時に、アプリケーションの一時停止(いわゆる、 stop-the-world )が発生するリスクを軽減しています。
  • 一方で、シングルジェネレーションの Shenandoah と比較して、全体的なアプリケーションスループットの低下(つまり、追加のアプリケーションオーバーヘッド)が 5 % 未満発生します。
  • 圧縮されたオブジェクトポインタのサポートを維持しています。
  • x64 および ARM64 アーキテクチャをサポートしています。

私たちは、これらのメリットを幅広いワークロードに一般化し、最終的には 32-bit x86 および ARM アーキテクチャに一般化できるよう取り組んでいます。

どのような仕組みになっているのですか?

Shenandoah は Red Hat で開発され、 OpenJDK 12 で最初にリリースされた、 Mostly Concurrent Garbage Collector です。 Shenandoah は、アプリケーションスレッドの実行中に未使用のメモリをコレクションし、メモリを使い果たす前にメモリを再利用するように競争させることで、 p99 の一時停止時間を 10 ミリ秒未満で達成しています。 Shenandoah はレースに負けないように努めますが、負けた場合は、終了するまで全てのアプリケーションスレッドが一時停止されます。

Shenandoah がレースに勝つのを助ける方法はいくつか検討できます。より多くのスレッド ( -XX : ConcGCThreads ) を与えることはできますが、 GC により多くのマシンリソースを投入することになり、アプリケーションのスループットが低下します。ヒューリスティックを調整することで、スループットを犠牲にして、よりアグレッシブに実行されるように、有利なスタートを切ることもできます。もしくは、メモリを増やして、アプリケーションスレッドが終了する前にヒープがいっぱいにならないようにすることもできます。これらの選択肢がどれも魅力的ではない場合、あなたは別の選択肢を持っています。それは、Shenandoah の「若い世代」です。

ガベージ・コレクションを複数の世代 (通常は2つだけ) に分けると、各コレクションサイクル中に実行される作業量が減少します。この手法は最近まで、 Shenandoah と ZGC を除くすべての JVM コレクタで使用されてきました。従来の Shenandoah コレクションサイクルでは、再利用される未使用メモリの量を最大化するために、ヒープ全体をカバーしています。つまり、ヒープは単一の世代(シングルジェネレーション)で構成されています。しかし、新しくアロケーションされたオブジェクトのほとんどは、すぐに到達不能になるため、若い世代の別のヒープ領域にオブジェクトをアロケーションし、そこでコレクション作業に集中させると、最小限の労力で最大限の空きメモリが得られます。目的は、一時停止時間を減らすことではなく(これらはすでに非常に短い)、オブジェクトのコレクションとアロケーションの両方が発生するコンカレントサイクル時間を短縮することです。若い世代に注目することで、ガベージ・コレクタの競合が短くなり、アプリケーションの長い一時停止を回避できます。

Parallel および G1 コレクタで GC 関連の一時停止が長くなって苦労された開発者の方は、おそらく古い世代について疑問に思われているのではないでしょうか。構成可能な数の若いコレクションから生き残った若いオブジェクトは、若い世代に比べてコレクションされる頻度が低い古い世代にコピーされます。パフォーマンスのヒューリスティックに基づいて、Shenandoah のジェネレーショナルモードは、古い世代のメモリが使い果たされる前に、古い世代のコレクションを開始します。アプリケーションと若いコレクタの両方を実行しながら、古い世代を同時にコレクションします。若いコレクションを優先させる割り込み可能な同時進行の古いコレクションにより、古いコレクションが原因で Shenandoah ジェネレーショナルモードがアプリケーションとの競争に負けないようにします。

結果

Shenandoah ジェネレーショナルモードは、「実世界」のワークロードを表現されるように設計された Dacapo suite のベンチマークの一部で、有望な結果が示しましたが、すべてのベンチマークが実際のコレクタの違いを示すほど、十分なストレスを与えるわけではありません。これらのベンチマークは、-Xmx と -Xms を 8 GB とし、すべてのコレクタのデフォルト引数を指定して実行しました。データは2週間にわたって CI/CD パイプラインからコレクションされ、これらには、x86 および AArch64 Linux large build instances で走った約 420 回の実行を含みます。結果は、AWS OpenSearch に保存し、Kibana の vega-lite 統合を使用してレンダリングしています。

下の画像では、測定分布を「箱ひげ図」のテーブルを示しています。「ひげ」は p5 と p95 の値で、「ボックス」のエッジは p25 と p75 の値です。真ん中の線は p50 です。各行は Dacapo suite の異なるベンチマークです。各列はベンチマークの指標です。小さい値の方が良い値です。「 Elapsed Time 」列は、経過時間のベンチマークです。こちらも、小さいほど良い値です。「 Max Pause 」 は、 jHiccup ツールで測定された、観測された最大一時停止時間です。「 Max RSS 」列は、ベンチマーク実行中にプロセスで観測された Resident Set Size ( RSS ) の最大値です。

具体的にするために、上のグラフで batik ベンチマークでの一時停止時間をご覧ください(訳者注: batik ベンチマークは、Apache Batik のユニットテストに基づいて、多数の Scalable Vector Graphics ( SVG ) 画像を生成します)。これは次のように読み取ることができます。過去 2 週間のすべての実行において、G1で観測された最悪の一時停止時間( p95 )は約 750 ms で、中央値の一時停止時間( p50 )は約 575 ms でした。公平を期すために、 G1 は通常、他のコレクタよりもメモリ使用量が少なく、ベンチマークスコアからは一般的にうまく機能していることもはっきりと読み取れます。別の例として。下のグラフで示した xalan ベンチマークでは、 Shenandoah ジェネレーショナルモードに必要な最大 RSS は、シングルジェネレーショナルモードで必要とされる最大の RSS の半分以下であり、一時停止時間やベンチマークスコアに大きな差はありません。(※訳者注: xalan ベンチマークは、XML Document を HTML に変換します)

別のベンチマークの結果を次に示します。HyperAlloc は、オープンソースベンチマークスイート Heapothesys の一部です。このグラフは、8 GB のヒープに 1 GB のライブオブジェクトを保持し、アロケーションレートを 2 GB/s と 3 GB/s に設定した場合の一時停止時間の分布を示しています。ジェネレーショナルモードは、シングルジェネレーショナルモードよりも一時停止時間が短くなることがわかります。

どうやって使いますか?

Linux x86 および AArch64 ホスト用の実行可能バイナリをダウンロードするためのリンクは、Shenandoah ジェネレーショナルモードの read-me ページにあります。

ジェネレーショナル機能を有効にするには、Java コマンドラインで次のオプションを使用して Shenandoah のモードを変更します。

-XX:+UseShenandoahGC -XX:+UnlockExperimentalVMOptions
-XX:ShenandoahGCMode=generational

もちろん、ジェネレーショナルモードのコマンドラインオプションは他にもありますが、その説明はこの記事では扱いません。これらを表示するには、 XX:+PrintFlagsFinal を実行してください。表示された中から「 Shenandoah 」を grep 等で探してください。前述の理由から、ジェネレーショナルモードでは、現在のアプリケーションよりも大きな若い世代を必要とする場合があります。若い世代のサイズを調整するには、 -XX:NewRatio を使用するか、より直接的には -XX:NewSize/-Xmn を使用します。ジェネレーショナルモードは、 -XX:InitialTenuringThreshold も理解できます。これは、オブジェクトが古い世代にコピーされる前に、存続しなければならないコレクションサイクルの数を制御するために使用されます。今後、若い世代のサイズを動的に調整するヒューリスティックな機能が追加される予定です。今のところは起動時に固定されています。

Shenandoah のジェネレーショナルモードは現在 Preview 版です。しかし、私たちは、お客様がこれを本番環境に導入できるようになることをご支援したいので、お客様と協力させて頂きたいと考えています。GitHub チケットで弊社にご連絡ください。速やかにお返事いたします。

メトリック

Shenandoah ジェネレーショナルモードには、ガベージコレクタの実行に関するインサイトを提供する、新しい詳細なメトリクスが追加されています。メトリクスは、コレクションを 1 つのイベントとして扱うのではなく、さまざまなコレクタのフェーズと、それらが同時に実行されるかどうかを公開します。追加情報には、アプリケーションのアロケーションや、コレクタが同時に実行される wall-clock time の、合計に対する割合が含まれます。(※訳者注: wall-clock time は開始から終了までにかかった実際の時間です。)

すべてのメトリクスは、GarbageCollectorMXBean を介して Java Management Extensions ( JMX ) で公開されます。JConsole などのツールを使用してそれらを取得したり、 JMX apis を使用して直接アクセスしたりできます。

JConsole が GC の一時停止フェーズに関する詳細情報を表示する例

上の例では、1.3 ミリ秒 ( 1,306,273 ナノ秒 ) の一時停止が報告されています。このような一貫して短い休止時間により、レイテンシーに敏感なアプリケーションの多くを Java で記述することができます。

GarbageCollectorMXBeanGcInfo javadoc には、使用可能なメトリクスと、その意味と詳細の完全なリストが記載されています。

もっと詳しく知りたいのですが、どうやって参加すればいいですか?

Shenandoah ジェネレーショナルモードは Work In Progress ( WIP ) です。重要なワークロードにメリットがあることは非常に喜ばしいことですが、改善が必要な領域がいくつかあります。

  1. 若い世代と古い世代のコレクションをいつ開始するかは、ヒューリスティックに決定されます。現在の実装では、コレクタが空きプールを補充する前に、オブジェクトのアロケーションによって空きプールが枯渇してしまうという、不要なコレクショントリガーラグが観察されています。
  2. アプリケーションペーシングにより、コレクションサイクル中に、利用可能なアロケーションプールが、コレクタの進行速度を超えないペースで消費されるように保証することができます。ペーシングの実装では、世代別コレクションの特別なニーズを考慮する必要があります。
  3. さまざまなパフォーマンスの向上が検討されています。開発の優先順位は、初期のお客様からのフィードバックに基づきます。

お客様からのご連絡をお待ちしております。今後のロードマップの改善にご協力ください。私たちは今後も Corretto と OpenJDK に投資し、Java 仮想マシンのパフォーマンスを向上させ、Java のイノベーションを推進していきます。

Corretto チームは、Corretto と Shenandoah ジェネレーショナルモードに関するフィードバックや質問に感謝します。corretto-17 リポジトリにある私たちのブランチは「 generational-shenandoah 」です。また、私達は OpenJDK Shenandoah プロジェクトリポジトリの default branch にもプッシュします。このブランチは corretto-17 ブランチよりも OpenJDK tip リポジトリに近いです。

私達のリポジトリへの問題の報告や機能のリクエストには、GitHub Issues をご利用ください。プルリクエストは大歓迎です。

最後に、RedHat のエンジニアとのコラボレーションは素晴らしく、実り多いものでした。Shenandoah プロジェクトのコントリビューターについては https://github.com/openjdk/shenandoah をご覧ください。

この記事の翻訳は、ソリューションアーキテクトの宮島 嶺が担当しました。