原文はこちら。
The original article was written by Thomas Schatzl (OpenJDK developer, Oracle).
https://tschatzl.github.io/2021/09/16/jdk17-g1-parallel-gc-changes.html
数日前、JDK 17が一般提供されました。
Java 17 / JDK 17: General Availability
https://mail.openjdk.java.net/pipermail/jdk-dev/2021-September/006037.html
このため、このリリースにおけるHotspotのStop-the-Worldガベージコレクタの最も重要な変更点(G1とParallel GC)をまとめたまた別の記事を投稿することにしました。
G1とParallel GCで何が変わったかを詳しく説明する前に、GCサブコンポーネント全体の統計を使って簡単な概要を説明します。なお、JDK 17においてGC領域のJEPはありませんでした。
JDK 17
https://openjdk.java.net/projects/jdk/17/
HotSpot GCサブコンポーネント全体の変更点のリストはこちらからどうぞ。全体で312個の変更があります。これは最近のリリースと一致しています。
stop-the-worldコレクターの話をする前に、ZGCについて簡単に説明します。
ZGC
https://wiki.openjdk.java.net/display/zgc/Main
今回のリリースでは、アプリケーションに合わせてconcurrent GCスレッドを動的に調整することで、スループットの最適化とアロケーションストールの回避を両立させ、使い勝手を向上させました(JDK-8268372)。
[JDK-8268372] ZGC: dynamically select the number of concurrent GC threads used
https://bugs.openjdk.java.net/browse/JDK-8268372
その他の重要な変更点としては、JDK-8260267でマークスタックのメモリ使用量が大幅に削減されています。詳細は、近日中にPer Lidenがブログにする予定です(訳注:2021/09/25現在まだのようです)。
[JDK-8260267] ZGC: Reduce mark stack usage
https://bugs.openjdk.java.net/browse/JDK-8260267
Generic improvements
JDK-8256155以降、VMは異なるメモリ予約に対して異なるサイズのラージページを使用できるようになりました。
[JDK-8256155] Allow multiple large page sizes to be used on Linux
https://bugs.openjdk.java.net/browse/JDK-8256155
例えば、コードキャッシュでは、Javaヒープやその他の大容量予約とは異なるサイズのラージページを使用できます。これにより、システムに設定されたラージページをよりうまく使用できます。私の同僚のStefanがラージページを使用する理由と方法について書いています。
Large pages and Java
https://kstefanj.github.io/2021/05/19/large-pages-and-java.html
https://logico-jp.io/2021/05/26/large-pages-and-java/
Parallel GC
Parallel GCのポーズ(一時停止)は、これまでポーズにおける直列に行われていたフェーズを並行実行することで、少し高速化しました。これには以下の内容が含まれています。
G1のような動的並列参照処理を実装しているJDK-8204686は、実装に長時間かかっていましたが、直近の数リリースでの作業により、この機能を簡単に実装できました。これでG1の実装と同じように動作します。
[JDK-8204686] Dynamic parallel reference processing support for Parallel GC
https://bugs.openjdk.java.net/browse/JDK-8204686
GCにおいて所定の種類の参照(Soft、Weak、Final、Phantom)の処理を必要とする java.lang.ref.Referenceインスタンスの個数に基づき、Parallel GCは参照処理のとあるフェーズのために異なる個数のスレッドを開始するようになりました。大まかにいうと、この実装では、指定されたフェーズで観測された j.l.ref.Reference
の数を ReferencesPerThread
の値(デフォルト値は 1000
)で割り、その値でParallel GC がその特定のフェーズで使用するスレッドの量を決定しています。
ParallelRefProcEnabled
オプションがデフォルトで有効になっており、このメカニズムが有効になっています。JDK 11のG1でこの機能が導入されて以来、不満の声は聞かれませんでしたので、これは適切だと思われます。リリースノートもご確認ください。
[JDK-8043575] Dynamically parallelize reference processing work
https://bugs.openjdk.java.net/browse/JDK-8043575
JDK 17 Release Notes
https://jdk.java.net/17/release-notes#JDK-8204686
同様に、すべての内部弱参照の処理がJDK-8268443で自動的に並列実行されるように変更されています。
[JDK-8268443] ParallelGC Full GC should use parallel WeakProcessor
https://bugs.openjdk.java.net/browse/JDK-8268443
最後になりますが、JDK-8248314では同じ理由でFull GCのポーズが数ミリ秒短縮されています。
[JDK-8248314] Parallel: Parallelize parallel full gc Adjust Roots phase
https://bugs.openjdk.java.net/browse/JDK-8248314
また、一部のアプリケーションではJDK 16と比較して、一桁パーセントという小さなスループットの改善が見られましたが、これはGCよりもJDK 17のコンパイラの改善に関連している可能性が高いです。つまり、上記の変更がアプリケーションの問題を正確に解決していないのであれば、コンパイラの改善によるものだ、ということです。
G1 GC
G1は、JDK-8257774 で予防的なガベージコレクション(preventive garbage collection)をスケジュールするようになりました。
[JDK-8257774] G1: Trigger collect when free region count drops below threshold to prevent evacuation failures
https://bugs.openjdk.java.net/browse/JDK-8257774
これはMicrosoftによる貢献で、特別な種類のyoungコレクションを導入し、退避失敗による典型的な長いポーズを回避しようというものです。
Behavior in Very Tight Heap Situations – Garbage-First Garbage Collector Tuning
https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html#GUID-BE157AF6-29E7-461A-82CF-50C1978785DA
短命なhumongousオブジェクトの割り当て率が高い場合、オブジェクトをコピーするのに十分なスペースがないという状況がよく発生します。こうしたオブジェクトは、G1が通常のガベージコレクションをスケジュールする前にヒープを埋め尽くしてしまう可能性があります。
そこでG1は、このような状況が起こるのを待たずに、生存オブジェクトをコピーするのに十分なスペースがあると確信できる間に、スケジュール外のガベージコレクションを開始します。これは、eager reclaim(大型オブジェクトからの回収)によって多くのヒープスペースが解放され、通常のオペレーションが継続できると想定してのことです。
Periodic Garbage Collections – Garbage-First Garbage Collector Tuning
https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html#GUID-D74F3CC7-CC9F-45B5-B03D-510AEEAC2DAC
予防的ガベージコレクションは、ログにG1 Preventive Collection
としてタグ付けされます。つまり、対応するログエントリは以下のようになります。
[...]
[2.574s][info][gc] GC(121) Pause Young (Normal) (G1 Evacuation Pause) 86M->83M(90M) 5.781ms
[2.582s][info][gc] GC(122) Pause Young (Normal) (G1 Evacuation Pause) 86M->83M(90M) 4.936ms
[2.596s][info][gc] GC(123) Pause Young (Normal) (G1 Preventive Collection) 86M->84M(90M) 9.997ms
[...]
予防的ガベージコレクションはデフォルトで有効になっています。リグレッションが発生した場合には、診断フラグであるG1UsePreventiveGC
を使って無効化できます。
Windows でのラージ・ページ処理に関する重要なバグが修正されました。JDK-8266489では、領域サイズが2MBより大きい場合、G1がラージページを使用することが可能になり、より大きなJavaヒープ上でのパフォーマンスが大幅に向上するケースがあります。
[JDK-8266489] Enable G1 to use large pages on Windows when region size is larger than 2m
https://bugs.openjdk.java.net/browse/JDK-8266489
JDK-8262068で、Hamlin Liは、Serial GCおよびParallel GCに加えて、G1 Full GCでのMarkSweepDeadRatio
オプションのサポートを追加しました。
[JDK-8262068] Improve G1 Full GC by skipping compaction for regions with high survival ratio
https://bugs.openjdk.java.net/browse/JDK-8262068
このオプションは、圧縮(compaction)が予定されている領域で、どの程度の無駄を許容するかを制御します。生存オブジェクトがこの比率(デフォルトでは95%)よりも多く存在するリージョンは圧縮されません。これは、圧縮してもそれほど多くのメモリを回収できず、圧縮だけに時間がかかるからです。
状況によっては、これが望ましくない場合もあります。何らかの理由でヒープを最大限に圧縮したい場合は、このフラグの値を100
に設定すると、この機能が無効になります(他のGCと同様です)。
コレクションセットを早期に刈り込むことで、メモリを大幅に節約できる可能性があります(JDK-8262185)。
[JDK-8262185] G1: Prune collection set candidates early
https://bugs.openjdk.java.net/browse/JDK-8262185
この変更により、G1は、すべての有用な候補ではとはかぎらないが、ほぼ確実に退避するold generationリージョンの範囲についてのみ、記憶集合(remembered sets)を維持しようとします。過去のエントリで、この問題の概要と潜在的なベネフィットを示しています。
Welcome 20% less memory usage for G1 remembered sets – Prune collection set candidates early
https://tschatzl.github.io/2021/02/26/early-prune.html
https://logico-jp.io/2021/02/28/welcome-20-less-memory-usage-for-g1-remembered-sets-prune-collection-set-candidates-early/
以下のエントリで報告されているように、GCフェーズの一部をさらに並列化することで、結果として全体的なパフォーマンス向上がはかられる可能性があります(例:JDK-8214237)。
[JDK-8214237] Join parallel phases post evacuation
https://bugs.openjdk.java.net/browse/JDK-8214237
How much faster is Java 17?
https://www.optaplanner.org/blog/2021/09/15/HowMuchFasterIsJava17.html
Other Noteworthy Changes
さらに、エンドユーザーにはあまり見えないけれども重要な変更がJDK 17に入っています。
G1GCのコードのリファクタリングを積極的に開始しました。特に、catch-allクラスであるG1CollectedHeap
からコードを移動させ、関心を分離し、より理解しやすいコンポーネントに切り分けようとしているところです。これにより、メンテナンス性が向上し、今後の作業のスピードアップにつながることを期待しています。
What’s next
もちろん、GCチームと他のコントリビューターはすでにJDK 18に向けて作業をしています。ここでは、現在開発中の興味関心をお持ちになる可能性のある変更点を少々ご紹介します。保証はありませんが、いつものように、作業が完了したら統合される予定です。
まず、実際にすでに統合されている変更(JDK-8017163)は、コストをかけずにG1のメモリ消費量を大幅に削減します。この記憶集合のデータストレージを書き換えたことにより、そのフットプリントはJDK 17からJDK 18で約75%削減されます。
[JDK-8017163] G1: Refactor remembered sets
https://bugs.openjdk.java.net/browse/JDK-8017163
Reference Object Processing Takes Too Long – Garbage-First Garbage Collector Tuning
https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-garbage-collector-tuning.html#GUID-A0343B53-A690-4DDE-98F9-9877096DBF0F
下図は、様々な最近のJDKのティーザーとして、とあるデータベースのようなアプリケーションのGCコンポーネントについて、NMTによって報告されたメモリ消費量を示しています。
特に、17b35(ピンク)と16.0.2(青)と比較して、現在(18b11)の総メモリ使用量を示す黄色の線に注目してください。この曲線から”floor”(シアン)で表される他のGCコンポーネントのメモリ使用量を差し引くことで、記憶集合のサイズを算出できます。
この変更については、今後このブログでより詳細な評価と説明を行う予定です。少なくとも、記憶集合のサイズチューニングは過去のものとなるでしょう。今回の変更をベースに、パフォーマンスを向上させ、記憶集合のメモリサイズをさらに小さくするためのさらなる変更が予定されています。
JDK 18では、Serial GC、Parallel GC、およびZGCで、G1やShenandoahのような文字列重複排除をサポートしています。JEP 192にはこの技術の詳細が記載されており、現在はすべてのHotspot GCに適用されています。
JEP 192: String Deduplication in G1
https://openjdk.java.net/jeps/192
Serial GCのアーカイブ済みヒープオブジェクトのサポートは、JDK-8273508で開発中です。
[JDK-8273508] Support archived heap objects in SerialGC
https://bugs.openjdk.java.net/browse/JDK-8273508
将来的にG1のリージョンピンニングを可能にするという明確な目標を掲げ、Hamlin Liが退避失敗時の処理の改善に取り組んでいます。以前、問題点と可能なアプローチについてちょこっと記事を書いています。
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/
まだまだ続きますよ。
Thanks go to…
今回の素晴らしいJDKのリリースに貢献してくれた皆さん。次のリリースでお会いしましょう。