Introducing Generational ZGC

原文はこちら。
The original article was written by Billy Korando (Developer Advocate at Oracle).
https://inside.java/2023/11/28/gen-zgc-explainer/

Javaの高度にスケーラブルで低レイテンシーのガベージ・コレクターであるZGCは、JDK 21で更新され、JEP 439でgenerational GC (世代別GC) になりました。

JEP 439: Generational ZGC
https://openjdk.org/jeps/439

では、Generational ZGCをどのように使うのでしょうか?また、Generational ZGCに切り替えることで、どのようなパフォーマンスが得られるのでしょうか?覗いてみたいと思います。

What is ZGC?

ZGCはまず実験的な機能としてJDK 11でリリースされ、JDK 15で本番機能にアップグレードされました。ZGCは非常にスケーラブルに設計されており、最大16TBのヒープをサポートしながら、ミリ秒以下のポーズ時間を維持します。

ZGCは、ほぼ完全に並行処理することで、これらの目標を達成しています。つまり、ZGCはアプリケーションの実行中に、新しいオブジェクトの割り当て、到達不可能なオブジェクトのスキャン、ヒープのコンパクト化などの作業を行っているのです。

この設計上の選択によるトレードオフは、アプリケーションが使用できるCPUリソースをZGCが代わりに使用するため、アプリケーションのスループットが低下する、というものです。

What is Generational ZGC?

generational GC (世代別GC) は、論理的にヒープを2つの世代 (generation)、つまりyoung generationとold generationの2世代に分けます。オブジェクトが割り当てられると、最初はyoung generationに置かれ、頻繁にスキャンされます。オブジェクトが十分に長く生き残ると、old generationに昇格します。

generational GCがこのような動作をするのは、ほとんどのオブジェクトは生成された直後に到達不可能になる(短命である)、という弱世代仮説(weak-generational hypothesis) を利用するためです。

そのため、ZGCはyoung generationを頻繁にスキャンすることで、CPUリソースをより効率的に使うことができます。

Generational ZGCの開発中、ZGCのエンジニアリング・チームは定期的に社内でパフォーマンス・テストを実施し、Generational ZGCが目標を達成しているかどうかを確認しました。

スループットについては、世代別ZGCはJDK 17の単世代ZGCと比較して約10%改善し、JDK 21の単世代ZGCと比較した場合、少々regressionが見られたために、10%強改善しました。

下図 (Latency) は前のThroughputとほぼ同じに見えますが、異なるものです。具体的には、世代別ZGCが単世代ZGCと比較して平均レイテンシがわずかに悪化したことを示しています。

しかしながら、実際の数値を見ると、差はわずか2~3マイクロ秒であることがわかります。

最大ポーズ時間を見ると、ZGCが輝きを見せ始めます。下図では、ポーズ時間のP99が10~20%改善され、JDK 21とJDK 17の単世代ZGCと比較して、それぞれ実数で20マイクロ秒と30マイクロ秒の改善が見られます。

世代別ZGCの最大の利点は、単世代ZGCの最大の問題であるアロケーションストール(allocation stall)の可能性を大幅に減らす点です。アロケーションストールとは、新しいオブジェクトのアロケーション速度が、ZGCがメモリを再利用できる速度よりも速い場合を意味します。

この問題は、ユースケースをApache Cassandraに切り替え、99.999パーセンタイルを見るとわかります。下図から、同時クライアント数が75台までは、単世代ZGCと世代別ZGCのパフォーマンスがほぼ同じであることを示しています。しかし、同時クライアント数が75を超えると、単世代ZGCは過負荷になり、アロケーションストールの問題に直面します。一方、世代別ZGCではこの問題は発生せず、275もの同時クライアントがあっても一貫したポーズ時間を維持します。

アロケーションストール問題や世代別ZGCについてもっと知りたい方は、Erik OsterlündのJVMLSでのプレゼンテーション (Generational ZGC and Beyond) をぜひご覧ください。

Generational ZGC and Beyond

Using ZGC

ZGCへの世代別の挙動を実装するのは大幅な変更を伴ったため、ZGCチームは単世代ZGCから世代別ZGCへの移行期間を設定しました。JDK 21では、ZGCを使用する場合、単世代ZGCがデフォルトのままですが、最終的には、将来のリリースで世代別ZGCがデフォルトとなり、単世代は非推奨となり、その後削除される予定です。しかし、これらのステップのスケジュールはまだ決まっていません。

JDK21で世代別ZGCを使用するには、以下の2つのJVM引数が必要です。

$java -XX:+UseZGC -XX:+ZGenerational

Tuning ZGC

ZGCはセルフチューニングできるように設計されています。ほとんどの場合、ユーザーが提供すべき設定は最大ヒープ(-Xmx)だけです。しかし、追加設定が必要な場合もあります。以下は、検討に値するいくつかの主要な設定です。

設定意味
-XX:SoftMaxHeapSize=<size>この引数で、ZGCが以下にとどまろうとするヒープサイズのガイドラインを提供します。しかし、アロケーションの問題を避けるために、ZGCはこの制限を超えることもあります。ZGCはできるだけ早くSoftMaxHeapSize以下に戻り、メモリをOSに返そうとします。

レイテンシが主要な懸念であれば、検討に値する設定がいくつかあります。

設定意味
-Xms最小ヒープサイズ。
この値は最大ヒープサイズ (-Xmx) と一致させること。これにより、待ち時間の原因となる、要求されていないメモリをZGCがOSに返さないようにできます。
-XX:-ZUncommitこの設定で、OSにメモリを返さないようにすることもできます。
-XX:ZUncommitDelay=<seconds>ZGCがOSにメモリを戻すまでの待機時間を管理します。デフォルトは300秒です。
-XX:+AlwaysPreTouch起動中にヒープの準備をするように構成します。これにより、起動が少し遅くなりますが、平均待ち時間が短縮されます。

Profiling ZGC

世代別ZGCを評価して世代別ZGCに切り替えるかどうかを確認したり、チューニング変更の影響を測定したりするのではなく、ZGCをプロファイリングしてZGCを正確に評価する必要があります。GCに関する診断情報を収集する主な方法は2つ、GCロギングとJDKフライトレコーダーです。

GC Logging

Since JDK 9から、JVMログを使うことで、より質の高いデータに簡単にアクセスできるようになりました。これは、JDK 9に含まれる2つのJEP、158と271の結果です。この両JEPにより、JVMログがGCを評価するときの素晴らしい選択肢たらしめています。

JEP 158: Unified JVM Logging
https://openjdk.org/jeps/158
JEP 271: Unified GC Logging
https://openjdk.org/jeps/271

以下のように引数-Xlogを使ってJVMログを構成します。

$ java -Xlog:gc:gen-zgc.log

このコマンドは、gcでタグ付けされたログだけをキャプチャし、gen-zgc.logファイルにパイプします。

より広範な GC ログを取得する場合には、例えば以下のような設定を利用できます。

$ java -Xlog:gc*:gen-zgc.log 

このコマンドは、タグgcを含む全てのログをキャプチャします。このコマンドは、以下の例のようなGC統計の表も出力します。

7.731s][info][gc,stats ] === Garbage Collection Statistics =======================================================================================================================
[7.731s][info][gc,stats ] Last 10s Last 10m Last 10h Total
[7.731s][info][gc,stats ] Avg / Max Avg / Max Avg / Max Avg / Max
[7.731s][info][gc,stats ] Contention: Mark Segment Reset Contention 23 / 164 23 / 164 23 / 164 23 / 164 ops/s
[7.731s][info][gc,stats ] Contention: Mark SeqNum Reset Contention 0 / 2 0 / 2 0 / 2 0 / 2 ops/s
[7.731s][info][gc,stats ] Critical: Allocation Stall 0 / 0 0 / 0 0 / 0 0 / 0 ops/s
[7.731s][info][gc,stats ] Critical: Allocation Stall 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 ms
[7.731s][info][gc,stats ] Critical: JNI Critical Stall 0 / 2 0 / 2 0 / 2 0 / 2 ops/s
[7.731s][info][gc,stats ] Critical: JNI Critical Stall 0.328 / 0.402 0.328 / 0.402 0.328 / 0.402 0.328 / 0.402 ms
[7.731s][info][gc,stats ] Critical: Relocation Stall 0 / 0 0 / 0 0 / 0 0 / 0 ops/s
[7.731s][info][gc,stats ] Critical: Relocation Stall 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 ms
[7.731s][info][gc,stats ] Major Collection: Major Collection 131.713 / 176.653 131.713 / 176.653 131.713 / 176.653 131.713 / 176.653 ms
[7.731s][info][gc,stats ] Memory: Allocation Rate 252 / 424 252 / 424 252 / 424 252 / 424 MB/s
[7.731s][info][gc,stats ] Memory: Defragment 0 / 0 0 / 0 0 / 0 0 / 0 ops/s
[7.731s][info][gc,stats ] Memory: Out Of Memory 0 / 0 0 / 0 0 / 0 0 / 0 ops/s
[7.731s][info][gc,stats ] Memory: Page Cache Flush 0 / 0 0 / 0 0 / 0 0 / 0 MB/s
[7.731s][info][gc,stats ] Memory: Page Cache Hit L1 68 / 142 68 / 142 68 / 142 68 / 142 ops/s
[7.731s][info][gc,stats ] Memory: Page Cache Hit L2 0 / 0 0 / 0 0 / 0 0 / 0 ops/s
[7.731s][info][gc,stats ] Memory: Page Cache Hit L3 34 / 143 34 / 143 34 / 143 34 / 143 ops/s
[7.731s][info][gc,stats ] Memory: Page Cache Miss 21 / 109 21 / 109 21 / 109 21 / 109 ops/s
[7.731s][info][gc,stats ] Memory: Uncommit 0 / 0 0 / 0 0 / 0 0 / 0 MB/s
[7.731s][info][gc,stats ] Memory: Undo Object Allocation Failed 0 / 0 0 / 0 0 / 0 0 / 0 ops/s
[7.731s][info][gc,stats ] Memory: Undo Object Allocation Succeeded 1 / 10 1 / 10 1 / 10 1 / 10 ops/s
[7.731s][info][gc,stats ] Memory: Undo Page Allocation 0 / 2 0 / 2 0 / 2 0 / 2 ops/s
[7.731s][info][gc,stats ] Minor Collection: Minor Collection 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 ms
[7.731s][info][gc,stats ] Old Generation: Old Generation 29.325 / 33.113 29.325 / 33.113 29.325 / 33.113 29.325 / 33.113 ms
[7.731s][info][gc,stats ] Old Pause: Pause Mark End 0.017 / 0.021 0.017 / 0.021 0.017 / 0.021 0.017 / 0.021 ms
[7.731s][info][gc,stats ] Old Pause: Pause Relocate Start 0.016 / 0.022 0.016 / 0.022 0.016 / 0.022 0.016 / 0.022 ms
[7.731s][info][gc,stats ] Old Phase: Concurrent Mark 1.080 / 1.545 1.080 / 1.545 1.080 / 1.545 1.080 / 1.545 ms
[7.731s][info][gc,stats ] Old Phase: Concurrent Mark Continue 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 ms
[7.731s][info][gc,stats ] Old Phase: Concurrent Mark Free 0.003 / 0.009 0.003 / 0.009 0.003 / 0.009 0.003 / 0.009 ms
[7.731s][info][gc,stats ] Old Phase: Concurrent Process Non-Strong 11.434 / 14.918 11.434 / 14.918 11.434 / 14.918 11.434 / 14.918 ms
[7.731s][info][gc,stats ] Old Phase: Concurrent Relocate 0.212 / 0.558 0.212 / 0.558 0.212 / 0.558 0.212 / 0.558 ms
[7.731s][info][gc,stats ] Old Phase: Concurrent Remap Roots 13.610 / 14.563 13.610 / 14.563 13.610 / 14.563 13.610 / 14.563 ms
[7.731s][info][gc,stats ] Old Phase: Concurrent Reset Relocation Set 0.001 / 0.001 0.001 / 0.001 0.001 / 0.001 0.001 / 0.001 ms
[7.731s][info][gc,stats ] Old Phase: Concurrent Select Relocation Set 2.374 / 2.427 2.374 / 2.427 2.374 / 2.427 2.374 / 2.427 ms
[7.731s][info][gc,stats ] Old Subphase: Concurrent Classes Purge 1.261 / 2.522 1.261 / 2.522 1.261 / 2.522 1.261 / 2.522 ms
[7.731s][info][gc,stats ] Old Subphase: Concurrent Classes Unlink 8.125 / 11.612 8.125 / 11.612 8.125 / 11.612 8.125 / 11.612 ms
[7.731s][info][gc,stats ] Old Subphase: Concurrent Mark Follow 0.269 / 0.393 0.269 / 0.393 0.269 / 0.393 0.269 / 0.393 ms
[7.731s][info][gc,stats ] Old Subphase: Concurrent Mark Root Colored 0.035 / 0.042 0.035 / 0.042 0.035 / 0.042 0.035 / 0.042 ms
[7.731s][info][gc,stats ] Old Subphase: Concurrent Mark Root Uncolored 0.745 / 1.078 0.745 / 1.078 0.745 / 1.078 0.745 / 1.078 ms
[7.731s][info][gc,stats ] Old Subphase: Concurrent Mark Roots 0.809 / 1.148 0.809 / 1.148 0.809 / 1.148 0.809 / 1.148 ms
[7.731s][info][gc,stats ] Old Subphase: Concurrent References Enqueue 0.000 / 0.001 0.000 / 0.001 0.000 / 0.001 0.000 / 0.001 ms
[7.731s][info][gc,stats ] Old Subphase: Concurrent References Process 0.038 / 0.058 0.038 / 0.058 0.038 / 0.058 0.038 / 0.058 ms
[7.731s][info][gc,stats ] Old Subphase: Concurrent Remap Remembered 10.987 / 12.023 10.987 / 12.023 10.987 / 12.023 10.987 / 12.023 ms
[7.731s][info][gc,stats ] Old Subphase: Concurrent Remap Roots Colored 1.373 / 1.870 1.373 / 1.870 1.373 / 1.870 1.373 / 1.870 ms
[7.732s][info][gc,stats ] Old Subphase: Concurrent Remap Roots Uncolored 0.994 / 1.218 0.994 / 1.218 0.994 / 1.218 0.994 / 1.218 ms
[7.732s][info][gc,stats ] Old Subphase: Concurrent Roots ClassLoaderDataGraph 0.806 / 1.619 0.806 / 1.619 0.806 / 1.619 0.806 / 1.619 ms
[7.732s][info][gc,stats ] Old Subphase: Concurrent Roots CodeCache 0.988 / 1.212 0.988 / 1.212 0.988 / 1.212 0.988 / 1.212 ms
[7.732s][info][gc,stats ] Old Subphase: Concurrent Roots JavaThreads 0.176 / 1.077 0.176 / 1.077 0.176 / 1.077 0.176 / 1.077 ms
[7.732s][info][gc,stats ] Old Subphase: Concurrent Roots OopStorageSet 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 ms
[7.732s][info][gc,stats ] Old Subphase: Concurrent Weak Roots OopStorageSet 0.382 / 2.474 0.382 / 2.474 0.382 / 2.474 0.382 / 2.474 ms
[7.732s][info][gc,stats ] System: Java Threads 22 / 40 22 / 40 22 / 40 22 / 40 threads
[7.732s][info][gc,stats ] Young Generation: Young Generation 101.015 / 141.473 101.015 / 141.473 101.015 / 141.473 101.015 / 141.473 ms
[7.732s][info][gc,stats ] Young Generation: Young Generation 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 ms
[7.732s][info][gc,stats ] Young Generation: Young Generation (Collect Roots) 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 ms
[7.732s][info][gc,stats ] Young Generation: Young Generation (Promote All) 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 ms
[7.732s][info][gc,stats ] Young Pause: Pause Mark End 0.023 / 0.033 0.023 / 0.033 0.023 / 0.033 0.023 / 0.033 ms
[7.732s][info][gc,stats ] Young Pause: Pause Mark Start 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 ms
[7.732s][info][gc,stats ] Young Pause: Pause Mark Start (Major) 0.016 / 0.018 0.016 / 0.018 0.016 / 0.018 0.016 / 0.018 ms
[7.732s][info][gc,stats ] Young Pause: Pause Relocate Start 0.011 / 0.013 0.011 / 0.013 0.011 / 0.013 0.011 / 0.013 ms
[7.732s][info][gc,stats ] Young Phase: Concurrent Mark 72.869 / 108.491 72.869 / 108.491 72.869 / 108.491 72.869 / 108.491 ms
[7.732s][info][gc,stats ] Young Phase: Concurrent Mark Continue 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 ms
[7.732s][info][gc,stats ] Young Phase: Concurrent Mark Free 0.001 / 0.001 0.001 / 0.001 0.001 / 0.001 0.001 / 0.001 ms
[7.732s][info][gc,stats ] Young Phase: Concurrent Relocate 16.027 / 17.168 16.027 / 17.168 16.027 / 17.168 16.027 / 17.168 ms
[7.732s][info][gc,stats ] Young Phase: Concurrent Reset Relocation Set 0.021 / 0.034 0.021 / 0.034 0.021 / 0.034 0.021 / 0.034 ms
[7.732s][info][gc,stats ] Young Phase: Concurrent Select Relocation Set 10.315 / 13.271 10.315 / 13.271 10.315 / 13.271 10.315 / 13.271 ms
[7.732s][info][gc,stats ] Young Subphase: Concurrent Mark Follow 67.482 / 102.707 67.482 / 102.707 67.482 / 102.707 67.482 / 102.707 ms
[7.732s][info][gc,stats ] Young Subphase: Concurrent Mark Root Colored 2.950 / 3.596 2.950 / 3.596 2.950 / 3.596 2.950 / 3.596 ms
[7.732s][info][gc,stats ] Young Subphase: Concurrent Mark Root Uncolored 2.384 / 3.325 2.384 / 3.325 2.384 / 3.325 2.384 / 3.325 ms
[7.732s][info][gc,stats ] Young Subphase: Concurrent Mark Roots 5.384 / 6.111 5.384 / 6.111 5.384 / 6.111 5.384 / 6.111 ms
[7.732s][info][gc,stats ] Young Subphase: Concurrent Relocate Remset FP 0.018 / 0.022 0.018 / 0.022 0.018 / 0.022 0.018 / 0.022 ms
[7.732s][info][gc,stats ] Young Subphase: Concurrent Roots ClassLoaderDataGraph 1.494 / 2.082 1.494 / 2.082 1.494 / 2.082 1.494 / 2.082 ms
[7.732s][info][gc,stats ] Young Subphase: Concurrent Roots CodeCache 1.498 / 2.258 1.498 / 2.258 1.498 / 2.258 1.498 / 2.258 ms
[7.732s][info][gc,stats ] Young Subphase: Concurrent Roots JavaThreads 0.445 / 1.065 0.445 / 1.065 0.445 / 1.065 0.445 / 1.065 ms
[7.732s][info][gc,stats ] Young Subphase: Concurrent Roots OopStorageSet 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 0.000 / 0.000 ms
[7.732s][info][gc,stats ] Young Subphase: Concurrent Weak Roots OopStorageSet 0.908 / 1.966 0.908 / 1.966 0.908 / 1.966 0.908 / 1.966 ms
[7.732s][info][gc,stats ] =========================================================================================================================================================
[7.732s][info][gc,heap,exit] Heap
[7.732s][info][gc,heap,exit] ZHeap used 394M, capacity 992M, max capacity 8192M
[7.732s][info][gc,heap,exit] Metaspace used 67936K, committed 68544K, reserved 1114112K
[7.732s][info][gc,heap,exit] class space used 9150K, committed 9408K, reserved 1048576K
view raw gen-zgc.log hosted with ❤ by GitHub

JVMログの詳細については、必ず公式ドキュメントをチェックしてください。

Enable Logging with the JVM Unified Logging Framework
https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html#enable-logging-with-the-jvm-unified-logging-framework

JDK Flight Recorder

JDK Flight Recorder (JFR) は、JDKに直接統合されたJavaのobservabilityと監視のフレームワークです。JFRの詳細については、次のStackWalkerエピソードをチェックしてください。

Java’s Observability and Monitoring Framework – JFR

JFRの起動と設定にはいくつかのオプションがありますが、GCを評価する場合、次の例のように、起動時に-XX:StartFlightRecordingで有効にしたいと思うはずです。

-XX:StartFlightRecording=filename=gen-zgc.jfr,settings=profile

これは、JFRで収集したデータをgen-zgc.jfrに書き込み、profile設定を使う、というものですが、プロファイリングのオーバーヘッドは2%以下です。あるいは、カスタム設定だけでなく、オーバーヘッドが1%以下のデフォルト設定を使用することもできます。

Improved Ergonomics
https://egahlin.github.io/2022/05/31/improved-ergonomics.html
https://logico-jp.io/2022/06/08/improved-ergonomics/

JFRデータが収集されたら、JDK Mission Control (JMC)で評価できます。JMCには、GCの概要、GCの設定、GCの動作の全体的なサマリーなど、GCの動作を評価するためのタブが用意されています。

JDK Mission Control
https://www.oracle.com/java/technologies/jdk-mission-control.html

注意: サマリーページの情報の一部は、少しずれているように見えるかもしれません。世代別ZGCとJMCの開発者の間で、世代別ZGCでyoung GCとold GCを最適に表現するための活発な議論が行われています。

Conclusion

世代別ZGCにより、ZGCがさらに多くのJavaアプリケーションにとって素晴らしい選択肢になることでしょう。ZGCはスケーラビリティと超低レイテンシを提供していますが、世代別機能が追加されたことで、アロケーションのストール問題はほぼ解決されました。JDK21へのアップグレードの際には、この機会に世代別ZGCを評価し、お使いのJavaアプリケーションに適しているかどうかを確認してください。

Additional Reading

The Z Garbage Collector
https://docs.oracle.com/en/java/javase/21/gctuning/z-garbage-collector.html#GUID-8637B158-4F35-4E2D-8E7B-9DAEF15BB3CD
ZGC Wiki
https://wiki.openjdk.org/display/zgc/Main
ZGC OpenJDK Dev Mailing List
https://mail.openjdk.org/mailman/listinfo/zgc-dev
Generational ZGC and Beyond
https://www.youtube.com/watch?v=YyXjC68l8mw

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください