Welcome 20% less memory usage for G1 remembered sets – Prune collection set candidates early

原文はこちら。
The original article was written by Thomas Schatzl (OpenJDK developer, Oracle).
https://tschatzl.github.io/2021/02/26/early-prune.html

長い間、記憶集合(Remembered sets、RSetとも)は G1GCの問題でした。最近、この分野で非常に良い影響を与える興味深い小さな変更がありました。

8262185: G1: Prune collection set candidates early #2693
https://github.com/openjdk/jdk/pull/2693

Introduction

G1 GCは、他のインクリメンタルコレクタと同様に、記憶集合を使用して、GC中にコピーする領域への参照の位置を格納します。結局のところ、オブジェクトをコピーした場合、そのオブジェクトへの参照は新しい場所に変更する必要があります。記憶集合とは、これらの場所を格納するデータ構造体のことです。

G1 はリージョンごとに記憶集合を格納して、領域ごとの退避を可能にしています。将来退避する可能性のあるG1領域間の参照をアプリケーションがインストールするたびに、小さなコードが実行され、参照が指すリージョンの記憶集合にその場所が追加されます。

ガベージファースト(G1)ガベージ・コレクタ : 基本概念
https://docs.oracle.com/javase/jp/15/gctuning/garbage-first-g1-garbage-collector1.html#GUID-E9CB81BC-92E5-489E-8A2E-760691A41CDF
Garbage-First (G1) Garbage Collector : Basic Concepts
https://docs.oracle.com/en/java/javase/15/gctuning/garbage-first-g1-garbage-collector1.html#GUID-E9CB81BC-92E5-489E-8A2E-760691A41CDF

Problem

では、これらの記憶集合はどれほどのメモリを利用できるのでしょうか?意外にも(意外でないかもしれませんが)結構なサイズを利用できます。

デモ目的で、この記事ではBigRamTesterを使用して正確にデモを実施します。この単純なアプリケーションはインメモリデータベース、つまりアイテムをLRU形式で常に追加したり削除したりする大きなハッシュテーブルをシミュレートしています。そのためJavaヒープは、これらの要素をあちこちで指し示す参照でいっぱいになり、記憶集合に負荷をかけ、大きな記憶集合を作成します。

Threads may do significant work out of the non-shared overflow buffer
https://bugs.openjdk.java.net/browse/JDK-8152438

以下のグラフは、20GBのJavaヒープ上で毎秒取得された、BigRamTester用のJava VMのNative Memory Tracking (NMT) が計算した “GC “コンポーネント用コミット済みメモリを示しています。これは結果として、最近のヒントでOpenJDKで使用されている16 MBの領域の選択につながります。

ネイティブ・メモリー・トラッキング
https://docs.oracle.com/javase/jp/15/vm/native-memory-tracking.html
Native Memory Tracking
https://docs.oracle.com/en/java/javase/15/vm/native-memory-tracking.html
8262094: Handshake timeout scaled wrong
https://bugs.openjdk.java.net/browse/JDK-8262094
https://github.com/openjdk/jdk/commit/9d9bedd051c313cf0f4552c6486c3f43bdaa81b9

指定したオプションは正確には以下の通りです。

$ java -XX:+UseLargePages -XX:+AlwaysPreTouch -Xmx20g -Xms20g -XX:MaxGCPauseMillis=500 -XX:+AlwaysPreTouch -XX:NativeMemoryTracking=summary BigRamTester 900
Current Memory Usage on BigRAMTester

紺色のメモリ消費量(baseline)に加えて、グラフでは、記憶集合ではなくGCコンポーネントが使用するメモリ量を示す黄色のfloorも示しています。今回の場合、G1の記憶集合は最大でも1.6 GB、java ヒープの約 8%を消費します。G1が操作に必要とする残りの部分の約 900 MBよりも大きなサイズです。

Background

G1には、young-onlyフェーズと領域回収(space reclamation)フェーズの2つの交互の動作フェーズがあります。young-onlyフェーズでは、G1はold世代のJavaヒープを徐々に埋めていきます。ある時点で、G1はold世代の領域を回収する時が来たと判断し、バックグラウンドで空きスペースを探し始めます。いわゆるマーキング完了後、G1は最終的に領域回収のために、コレクション・セットの候補である領域を選択します。その前に、これらの候補領域のための記憶セットを構築する必要があります。というのも、G1がyoung-onlyフェーズでそれらを維持しなかったためです(どうせold世代で領域回収するつもりはなかったので、維持する必要ないですよね)。

ガベージファースト(G1)ガベージ・コレクタ : ガベージ・コレクションによる一時停止とコレクション・セット
https://docs.oracle.com/javase/jp/15/gctuning/garbage-first-g1-garbage-collector1.html#GUID-3A99AE6C-F80A-4565-A27C-B4AEDF5CDF71
Garbage-First (G1) Garbage Collector : Garbage Collection Pauses and Collection Set
https://docs.oracle.com/en/java/javase/15/gctuning/garbage-first-g1-garbage-collector1.html#GUID-3A99AE6C-F80A-4565-A27C-B4AEDF5CDF71

記憶集合が完成するとすぐに、G1は領域回収フェーズに移行します。このフェーズでは、GCの都度コレクション・セット候補からいくつかのold世代領域の空間を集めて回収します。領域回収フェーズは、コレクションセット候補の中にこれ以上の領域がないか、または、そのフェーズでのGCポーズ後、領域回収のために残された空間が現在のヒープサイズの5%未満であるかのいずれかの場合に終了します。

ガベージファースト(G1)ガベージ・コレクタ : 領域回収フェーズの世代のサイズ設定
https://docs.oracle.com/javase/jp/15/gctuning/garbage-first-g1-garbage-collector1.html#GUID-6D6B18B1-063B-48FF-99E3-5AF059C43CE8
Garbage-First (G1) Garbage Collector : Space-Reclamation Phase Generation Sizing
https://docs.oracle.com/en/java/javase/15/gctuning/garbage-first-g1-garbage-collector1.html#GUID-6D6B18B1-063B-48FF-99E3-5AF059C43CE8

第2の条件を付けている理由は、候補リストの最後のいくつかの領域は、収集に比較的時間がかかる傾向があるからです。G1は効率性のために領域回収候補をソートします。その際、ある領域がどれだけの空き空間を生み出すかと、領域回収の困難さ、つまり、基本的にそれに含まれる記憶集合に含まれるエントリ個数を考慮します。G1は最も空きが多く、収集しやすい領域を最初に収集します。そのため、このルールでは、待ち時間のためにメモリを犠牲にして、一時停止時間が長くなりすぎるような領域をフィルタリングしています。

The change

非効率的な領域を回収しないという後者の条件を適用した場合のこの時間こそが、改善され得るものです。つまり、G1が決して領域回収しない領域用の記憶集合を効果的に維持します。さらに悪いことに、これらの記憶集合は、領域回収フェーズ終了まで維持されます。このため、これらの領域を回収している間は、記憶集合を保持する領域の数は減少するのに記憶集合の使用量は増加するという、驚くべき状況が発生する可能性があります。この効果は、上のメモリ使用量のグラフでも観察できます。記憶集合のメモリ使用量の最初の隆起の後には、記憶集合が大きくなっている様子が示されていますが、最終的に下がる前に最初の隆起よりもさらに高い第二の隆起があります。

ですから、この問題の簡単な解決策は、候補選択から記憶集合を作成するまでに、このルールを適用することです。

これには注意点があります。コレクション・セットの候補リストからすべての領域を削除してしまい、結果として、その空間の領域回収フェーズでは回収が進まないか、あるいはほとんど回収が進まないことになるかもしれませんが、これは望ましくありません。なぜなら、G1 は(ほとんど)何もないものに対しマーキングに労力を費やしているだけになってしまうからです。この理由から、また、オリジナルのheuristicsでは収集の最後にルールを適用していることをシミュレートするために、G1は、その領域回収フェーズで予想される混合コレクションの量に基づいて、少なくとも設定された量の領域を保持します。

BigRamTesterをこのパッチを適用した状態で再実行すると、以下のようなメモリ消費のグラフ(ピンク)が表示されます。

Impact of Early-Prune in Memory Usage on BigRAMTester

記憶集合のメモリ消費量の最大振幅で、250~300MBほどの差があります。公約の20%です。少なくともこのタイプのアプリケーションではこれぐらい削減できます。

実際にはもっと削減できるかもしれませんが、それはG1が現在どのように記憶集合のデータ構造を管理しているかに関係しています。これは別の機会に解決すべき問題です。


この変更は、記憶集合のメモリ消費量を削減するためのいくつかの長時間にわたる作業の一部です。

比較のために、同じベンチマークをJDK 10.0.2(シアン)で実施した場合におけるGCコンポーネントのメモリ消費量を示すグラフを付けました。この中には、上記比較のグラフも含めています。JDK 8と JDK 9では少なくともこれよりも悪いですが、記憶集合のNMTのアカウンティングのバグのために、正確な数字を得るのは困難です。

Fine bitmaps should be allocated as belonging to mtGC, not mtInternal
https://bugs.openjdk.java.net/browse/JDK-8176571

And what about earlier JDKs....

コメントを残す

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

WordPress.com ロゴ

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

Facebook の写真

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

%s と連携中