Shenandoah GCの性能指針と診断

Red Hat で Java Platform Advocate として OpenJDK を担当している伊藤ちひろ(@chiroito)です。

この記事は、OpenJDK のWikiページ、https://wiki.openjdk.java.net/display/shenandoah/Main の一部を翻訳した記事です。何回かに分けて翻訳していきますので、ぜひ最後までごらん下さい。

翻訳の各リンクはこちらです。

目次

性能指針と診断

一般的な考え方

ヒープサイズ: Shenandoahの性能は、ほとんど全ての他のGCの性能のように、ヒープサイズに依存します。並行フェーズ(アプリケーションと同時に動くフェーズ)が実行されている間、割り当てを収容するのに十分なヒープスペースがある場合、より良いパフォーマンスを発揮するはずです(下記の「障害モード」セクションを参照)。並行フェーズの時間は、生きているデータセットサイズ(LDS)、つまり生きているデータによって占有されるスペースと相関があります。 したがって、妥当なヒープサイズは、LDSと処理負荷からの割り当て具合に依存します。ある割り当て具合に対して、より大きなLDSは、比例してより大きなヒープサイズを必要とします。より大きなLDSに対して、より大きな割り当て具合はより大きなヒープサイズを必要とします。 極小のLDSと適度な割り当て具合を持つ一部の処理では、1~2GBのヒープが良好な性能を発揮します。私たちは日常的に、最大80%のLDSサイズを持つさまざまな処理で4~128GBヒープをテストしています。恥ずかしがらずに、さまざまなヒープサイズを試して、自分の処理に合うものを見つけてください。

停止:Shenandoahの停止動作は、ルートの走査と更新というルートセットの処理に大きく影響されます。ルートセットには、ローカル変数、生成されたコードに埋め込まれた参照、インターンされた文字列、クラスローダからの参照(例えば、静的なファイナル参照)、JNI参照、JVMTI参照が含まれます。ルートセットが大きいと、一般的にShenandoahでの停止時間が長くなります。これは、具体的なJDKのバージョンが、その仕事の一部を並行に行う機能を持ち、Shenandoahがそれを使用できる場合を除きます。 二次的な影響としては、a) 弱参照の処理 (ファイナルマーク停止中に発生)、ただし処理が必要な参照のみ、b) クラスのアンロードとその他のJDKクリーンアップ (こちらもファイナルマークの停止中に発生) が挙げられます。これらの二次的な影響は、追加のオプションを設定することで軽減できます。これらは、処理の頻度の制御(完全に無効にすることも含む)、および/または、アプリケーションを少し良く動作するように変更することです。

処理量(スループット):Shenandoahは並行処理GCであるため、収集サイクル中に不変性を維持するために障壁(バリア)を使用します。これらのバリアは、測定できるほどのスループットの損失を引き起こすかもしれません。そこで何が起こっているかを詳細に調べる方法については、以下の診断セクションを参照してください。一部のユーザーは、バリアによるスループットの低下は、空きコアや その他のアイドルコアに並行実行中の GC 処理を自然にオフロードすることで補われている と報告しています。言い換えれば、場合によっては、より高いアプリケーション+JVMの利用とより高いアプリケーションのスループットは引き換えになります。

ほとんどの場合、停止時間は0〜10ms以内、スループットの損失は0〜15%以内です。実際の性能の数値は、実際のアプリケーションや負荷の状態などに大きく依存します。ルートや弱参照、クラスの除去があまりないアプリケーションでは、停止はミリ秒以下の範囲に収まることがあります。ヒープをあまり変更しないアプリケーションや、現在のコンパイラで十分に最適化されているアプリケーションでは、バリアのオーバーヘッドはゼロに近くなる可能性があります。残りのセクションでは、Shenandoahの性能の振る舞いをテストし、診断するための手順について説明します。もし、あなたの具体的な使用例で何かおかしいと思ったら、開発者に知らせることを検討してください。可能性として、それは管理可能な問題か、真っ当なバグであることがあります。

基本設定

基本的な設定とコマンドラインオプションです。

  • -Xlog:gc (JDK 9 以降) または -verbose:gc (JDK 8 まで) は、個々の GC タイミングを表示します。
  • -Xlog:gc+ergo (JDK 9以降) または -XX:+PrintGCDetails (JDK8まで) または試行錯誤的な決定を表示します。これは、もしあれば、異常値を明らかにするかもしれません。
  • -Xlog:gc+stats(JDK9以降)または-verbose:gc(JDK8まで)は、実行の最後にShenandoah内部のタイミングに関する概要一覧を出力します。

ロギングを有効にして実行することは、ほとんどの場合、良いアイデアです。この概要一覧は、GCパフォーマンスに関する重要な情報を伝えます。そして、性能バグレポートでほぼ必然的に1つを求めることになります。試行錯誤的なログは、GCの異常値を把握するのに便利です。

その他に推奨されるJVMオプションは以下の通りです。

  • -XX:+AlwaysPreTouch:ヒープが使うページをメモリ上に前もって確保すること(コミット)は、遅延の小さな停止時間を軽減できます。

  • -Xms-Xmx-Xms = -Xmxでヒープサイズを変更不可にすることで、ヒープ管理による小さな停止時間を軽減します。 AlwaysPreTouch と組み合わせることで、-Xms = -Xmx は起動時にすべてのメモリを確保するため、メモリが最終的に使用されたときの小さな停止時間を回避できます。-Xms はまた、メモリの開放(アンコミット)の境界の下限を定義するので、-Xms = -Xmx では、すべてのメモリは確保されたままになります。つまり、Shenandoahをより低いリソース使用量で構成したい場合は、-Xmsを低く設定することが推奨されます。コミット/アンコミットの処理コストとメモリ使用量のバランスを取るために、どの程度低く設定するかを決定する必要があります。多くの場合、-Xmsを任意に低く設定することは問題ないでしょう。

  • ラージページを使用することで、大きなヒープでのパフォーマンスが大幅に改善されます。これを使用する方法は2つあります。-XX:+UseLargePagesはhugetlbfs(Linux)またはWindows(適切な特権を持つ)のサポートを有効にします。-XX:+UseTransparentHugePagesは、透過的にそれを有効にします。透過的なラージページでは、/sys/kernel/mm/transparent_hugepage/enabled/sys/kernel/mm/transparent_hugepage/defrag"madvise" に設定することが推奨されます。AlwaysPreTouchを付けて実行すると、起動時にデフラグのコストも前もって支払うことになります。

  • -XX:+UseNUMA: Shenandoahは、まだ明示的にNUMAをサポートしていません。ですが、マルチソケットのハードウェアでNUMAインターリーブを有効にするために、これを有効にするのは良いアイデアです。AlwaysPreTouchと組み合わせることで、デフォルトの設定よりも優れたパフォーマンスを提供します。

  • -XX:-UseBiasedLocking: 競合しない(バイアスド)ロッキングのスループットと、必要に応じて有効・無効を切り替えるためにJVMが行うセーフポイントとの間にはトレードオフがあります。遅延重視の負荷では、バイアスド・ロッキングをオフにすることは理にかなっています。

  • -XX:+DisableExplicitGC: ユーザーコードからSystem.gc()を起動すると、Shenandoahは追加のGCサイクルの実行を強制します; System.gc()を悪用するコードから守るために、これを無効にすると有益な場合があります。-XX:+ExplicitGCInvokesConcurrentはデフォルトで有効になっているので、通常は問題ないでしょう。これは、STW(Stop The World:アプリケーションが完全に停止する)のフルGCではなく、並行GCサイクルが呼び出されることを意味します。

モード

モードは、Shenandoahの主な実行方法を定義します。これは、もしあれば、Shenandoahがどのようなバリアを使っているかを定義し、主要な性能特性を定義します。モードは、-XX:ShenandoahGCMode=<名前>で選択できます。利用可能なモードは以下の通りです。

  1. normal/satb(製品、デフォルト)。このモードは、SATB(Snapshot-At-The-Beginning)マーキングと並行したGCを実行します。このマーキングモードは、G1が行っていることに似ています。G1は書き込みを妨げ、「以前の」オブジェクトを通してマーキングします。

  2. iu (実験的). このモードは増分更新(IU)マーキングと並行してGCを実行します。このマーキングモードは、SATBモードを反映します。これは、書き込みを阻止し、「新しい」オブジェクトを通してマークします。このため、特に弱参照にアクセスする場合、マークが甘くなることがあります。

  3. passive (診断)。このモードはstop-the-worldを伴うGCを実行します。このモードは機能テストに使われますが、時にはGCバリアで性能異常を二分したり(下記参照)、アプリケーションの実際の生きているデータサイズを把握するのに便利です。

ヒューリスティック

モードが選択された後、ヒューリスティックによって、ShenandoahがGCサイクルを開始するタイミングと、避難のために判断する領域を指示します。ヒューリスティックは、-XX:ShenandoahGCHeuristics=<名前>で選択できます。いくつかのヒューリスティックは、あなたの使用状況にGC操作をよりよく調整するのを助けるかもしれない構成パラメータを受け入れます。利用可能なヒューリスティックは、次のとおりです。

  1. adaptive (デフォルト).このヒューリスティックは、以前のGCサイクルを観察し、ヒープが枯渇する前に完了できるように、次のGCサイクルを開始しようとします。

    1. -XX:ShenandoahInitFreeThreshold=#: 「学習」コレクションの引き金となる初期閾値

    2. -XX:ShenandoahMinFreeThreshold=#: ヒューリスティックが無条件にGCを起動させる空き容量閾値

    3. -XX:ShenandoahAllocSpikeFactor=#: 割り当て急増を吸収するために確保すべきヒープ量

    4. -XX:ShenandoahGarbageThreshold=#: ゴミ収集のためにマークされる前に、その領域が含む必要のあるゴミの割合を設定します。

  2. static(以前は皮肉にもdynamicと呼ばれていた)。 このヒューリスティックは、ヒープの占有率に基づいてGCサイクルの開始を決定します。このヒューリスティックのチューニングの取っ掛かりは以下の通りです:

    1. -XX:ShenandoahMinFreeThreshold=#: GCサイクルを開始する空きヒープの割合を設定します。

    2. -XX:ShenandoahGarbageThreshold=#: 収集のためにマークされる前に領域が含むべきゴミの割合を設定します。

  3. compact(以前はcontinuousと誤って呼ばれていた)。このヒューリスティックは、GCサイクルを連続的に実行し、前のサイクルが終了するとすぐに次のサイクルを開始し、割り当てが発生する限り、実行します。このヒューリスティックは通常、スループットのオーバーヘッドを発生させますが、最も迅速なスペースの再利用を提供します。有用なチューニングの取っ掛かりは以下の通りです:

    1. -XX:ConcGCThreads=#: GCスレッドの並行処理数を削減し、アプリケーションの実行に余裕を持たせる。

    2. -XX:ShenandoahAllocationThreshold=#: 最後のGCサイクルから割り当てられたメモリのパーセンテージを、次のGCサイクルを開始する前に設定します。

  4. aggressive (診断)。このヒューリスティックは、GCが完全に活性化するように指示します。前のGCサイクルが終了するとすぐに新しいGCサイクルを開始し(「compact」のように)、すべての生きてるオブジェクトを退避させます。このヒューリスティックは、コレクタ自体の機能テストに便利です。しかし、大きなパフォーマンス・ペナルティが発生します。

障害モード

Shenandoahのような並列GCは、アプリケーションが割り当てるよりも速く収集することに暗黙のうちに依存しています。もし、割り当て要求が高く、GCの実行中に割り当てを受け入れるのに十分なスペースがない場合、最終的に割り当て失敗が発生します。Shenandoahは、このようなケースを乗り切るために、緩やかな縮退の梯子(Graceful Degradation Ladder)を持っています。この梯子は次のように構成されています。

  1. 歩調(-XX:+ShenandoahPacing、デフォルトで有効)。GCが実行されているとき、どれだけのGC作業が必要で、どれだけの空き領域がアプリケーションに利用可能であるかという考えを持っています。歩調の管理者は、GCの進捗が十分な速度でない場合、スレッドの割り当てを停止しようとします。通常の状態では、GC はアプリケーションが割り当てるよりも速く収集し、歩調の管理者は当然ながら失速することはありません。歩調は、ローカルなスレッド毎の遅延を導入することに注意してください。この遅延は通常のプロファイリングツールでは見えません。このため、失速は無限ではなく、-XX:ShenandoahPacingMaxDelay=#msで制限されています。最大遅延の期限が過ぎると、いずれにせよ割り当ては行われます。ほとんどの場合、軽度の割り当て急増は、歩調の管理者によって解決されます。割り当て圧力が非常に高い場合、歩調の管理者は対処できず、低下は次のステップに 進みます。通常の遅延は<10 msで引き起こされます。

  2. 縮退GC (-XX:+ShenandoahDegeneratedGC、デフォルトで有効)。アプリケーションが割り当て失敗に陥った場合、Shenandoahはstop-the-worldによる停止に移行し、アプリケーション全体を停止し、一時中断下でサイクルを継続させます。縮退GCは、stop-the-worldの下で進行中の「並行」サイクルを継続します。多くの場合、割り当て失敗は、かなりの量のGC作業がすでに行われた後に起こり、GC作業のごく一部を完了させる必要があります。これが、STWの停止が通常大規模にならない理由です。それはGCログでGC停止のように報告されるでしょう。これは、通常の監視や定期的な監視をするスレッドばかりです。実際、STW 停止を発生させる理由の 1 つは、並行モードの障害を明確に観察できるようにすることです。GCサイクルがあまりにも遅く開始された場合、または非常に大きな割り当て急増が発生した場合、縮退GCが発生する可能性があります。縮退したサイクルは、同時実行のサイクルよりも速いかもしれません。なぜなら、リソースをめぐってアプリケーションと争うことがなく、スレッドプールのサイジングに -XX:ConcGCThreads ではなく -XX:ParallelGCThreads を使用するためです。通常<100msの遅延が誘発されます。ですが、縮退ポイントによってはそれ以上になることもあります。

  3. フルGC。縮退したGCで十分なメモリが解放されなかった場合など、何をやってもダメな場合は、フルGCサイクルが発生し、ヒープを最大限まで圧縮してくれます。異常に断片化されたヒープと性能バグや見落としのような実装のようなあるシナリオは、フルGCによってのみ修正されるでしょう。この最後の手段のGCは、少なくともいくつかのメモリが利用可能である場合、アプリケーションがOOMで失敗しないことを保証します。通常100ms以上の遅延が発生します。特にヒープが非常に占有されている場合はそれ以上になることもあります。

個々の縮退GCとフルGCのイベントを表示する通常のGCログに加えて、-Xlog:gc+statsは実行の最後に次のようなものを表示します。

Under allocation pressure, concurrent cycles may cancel, and either continue cycle
under stop-the-world pause or result in stop-the-world Full GC. Increase heap size,
tune GC heuristics, set more aggressive pacing delay, or lower allocation rate
to avoid Degenerated and Full GC cycles.
(割り当て圧力がかかると、並行サイクルがキャンセルされ、Stop-the-Worldによる停止で
サイクルを継続するか、Stop-the-Worldを伴うフルGCとなる可能性があります。
ヒープサイズを増やす、GCのヒューリスティックを調整する、より積極的な歩調の遅延を設定する、
または縮退およびフルGCサイクルを回避するために割り当て率を下げて下さい)
 
 4912 successful concurrent GCs(成功した並行GC)
      0 invoked explicitly(明示的に起動)
 
    3 Degenerated GCs(縮退GC)
      3 caused by allocation failure(割り当て失敗による物)
         3 happened at Update Refs(参照の更新で発生)
      0 upgraded to Full GC(フルGCにアップグレードされた)
 
    0 Full GCs(フルGC)
      0 invoked explicitly(明示的に起動)
      0 caused by allocation failure(割り当て失敗による物)
      0 upgraded from Degenerated GC(縮退GCからアップグレードされた)
 
ALLOCATION PACING:(割り当ての歩調)
 
Max pacing delay is set for 10 ms.(最大の歩調の遅延は10msに設定)
 
Higher delay would prevent application outpacing the GC, but it will hide the GC latencies
from the STW pause times. Pacing affects the individual threads, and so it would also be
invisible to the usual profiling tools, but would add up to end-to-end application latency.
Raise max pacing delay with care.
(遅延が大きいと、アプリケーションがGCを追い越すのを防げます。
しかし、それはSTWの停止時間からGCの遅延を隠すことになります。
歩調は個々のスレッドに影響します。そのため、通常のプロファイリングツールでは見えません。
しかし、それは結局アプリケーションの遅延を増加させることになります。
歩調の遅延の最大値を上げるには、注意が必要です。)
 
Actual pacing delays histogram:(実際の歩調の遅れの推移)
      From -         To        Count
      1 ms -       2 ms:          87
      2 ms -       4 ms:         142
      4 ms -       8 ms:         297
      8 ms -      16 ms:        1733
     16 ms -      32 ms:          21
     32 ms -      64 ms:           1

このことから、アプリケーションがこれらの縮退のステップのいずれかに遭遇した場合に試すべきことがいくつかあります。

  • アプリケーションにもっとヒープを持たせる。これにより、GCが実行されているときに、より多くの割り当てを受けられるようになります。
  • ヒープ内の生きたデータの量を減らす。これにより、GCサイクルがより速く実行され、割り当てにうまく対処できるようになります。
  • 割り当ての圧力を減らしてください。例えば、割り当てを行うスレッドの数を減らす、またはアプリケーションの主要な割り当ての負荷を修正します。
  • できるだけ早くGCサイクルを開始するためにヒューリスティックをチューニングします。GCログによると、GCはすでに連続してサイクルを実行している場合、それは役に立たないかもしれません。
  • 歩調の遅延を増加させる。これは、縮退したGCやフルGCに昇格する代わりに、割り当てスレッドをより停止させるでしょう - これはまだそれらの割り当てスレッドに遅延をもたらすことに注意してください

性能分析

性能解析のアプローチ

  1. 割り当てに失敗してGCや長い最終マークのようないくつかのおかしな性能の挙動は、ヒューリスティックの問題で説明できます。-Xlog:gc+ergoは、そこでのあなたの味方になります。もし、長時間稼働する負荷がある場合、Shenandoah Visualizerで実行すると、高レベルのGCの挙動を理解できます。時々、おかしな挙動がそこではっきり見えることがあります。

  2. この性能差は、Shenandoahでの割り当て圧力がより大きいことで説明できます。それは各オブジェクトに転送ポインタが含まれるためです。それが問題かもしれないかどうか、割り当て率を見てください。そして、それを確認できるような実験をしてください(例えば、オブジェクトを改善することで、別のコレクタに対する性能差が減少するはずです)。メモリ使用量が大きいとCPUキャッシュから脱落するケースがあるので、L1/L2/LLCのミス差を調べてみてください。

  3. 多くのスループットの違いは、GCバリアのオーバーヘッドで説明できます。-XX:ShenandoahGCHeuristics=passiveで実行し、このヒューリスティックのみでは、バリアは不要なので、ヒューリスティックはそれらを無効にします。その後、選択的にバリアを有効に戻し、どのバリアがスループット性能に影響を与えているかを確認できます。"passive"ヒューリスティックが無効にしているバリアの一覧は、以下のようにGC出力に一覧化されます。

$ java -XX:+UseShenandoahGC -XX:ShenandoahGCHeuristics=passive -Xlog:gc
[0.002s][info][gc] Passive heuristics implies -XX:-ShenandoahSATBBarrier by default
[0.002s][info][gc] Passive heuristics implies -XX:-ShenandoahKeepAliveBarrier by default
[0.002s][info][gc] Passive heuristics implies -XX:-ShenandoahWriteBarrier by default
[0.002s][info][gc] Passive heuristics implies -XX:-ShenandoahReadBarrier by default
[0.002s][info][gc] Passive heuristics implies -XX:-ShenandoahStoreValReadBarrier by default
[0.002s][info][gc] Passive heuristics implies -XX:-ShenandoahCASBarrier by default
[0.002s][info][gc] Passive heuristics implies -XX:-ShenandoahAcmpBarrier by default
[0.002s][info][gc] Passive heuristics implies -XX:-ShenandoahCloneBarrier by default
[0.003s][info][gc] Using Shenandoah

4 . ネイティブGCコードのプロファイリングは、Linux perfで簡単にできます。

  1. . OpenJDK を --with-native-debug-symbols=internal でビルドすると、C++ コードへの対応が得られます。
  2. . perf record java ...で負荷を実行します。または perf record -g java ... で負荷を実行します。
  3. . perf report でレポートを開きます。
  4. . レポート内を移動し、疑わしいホットメソッド/パスがどこにあるかを確認します。メソッド上で "a "を押すと、通常そのメソッドの詳細な逆アセンブリが表示されます。

5 . バリアコードのプロファイリングには、PrintAssemblyが有効なビルドが必要です。JMH -prof perfasmを使って分離したシナリオを作成し、Shenandoahの下で生成されたコードを見ることをお勧めします。

通常のアプリケーションでは、GCによる停止だけが応答時間の大きな要因ではないと、理解することが重要です。大きな GC 停止があると、非常に高い確率で応答時間の問題が発生します。しかし、長い GC 停止がないからといって、常に適切な応答時間が得られるとは限りません。キューイング遅延、ネットワーク遅延、他のサービスの遅延、OSスケジューラのジッターなどがコストとして寄与している可能性があります。システムで何が起こっているかの全体像を把握するために、応答時間測定と共にShenandoahを実行することが推奨されます。これはその後、GC休止時間の統計と相関させるために使用できます。

例えば、これは、負荷の1つにjHiccupを使用したサンプルレポートです:

f:id:chiroito:20220407172758p:plain

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。