Heap Regions X-Large

原文はこちら。
The original article was written by Thomas Schatzl (OpenJDK developer, Oracle).
https://tschatzl.github.io/2021/11/15/heap-regions-x-large.html

これまでG1のJavaヒープの領域サイズ (region size) は、記憶セット (remembered set) のデータ構造における以前の制限のため、32MBに制限されていました。JDK-8275056により、JDK 18では、この制限が512MBに引き上げられます。

[JDK-8275056] Virtualize G1CardSet containers over heap region
https://bugs.openjdk.java.net/browse/JDK-8275056

この変更がどのように機能するのか、どのように使用できるのか、また、この変更による影響について簡単に説明します。

Introduction and Background

G1は、Javaヒープを均等なサイズの領域に分割し、段階的な退避を促進します。

Heap Layout
https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-g1-garbage-collector1.html#GUID-15921907-B297-43A4-8C48-DC88035BC7CF

ガベージコレクションの間に、死んだオブジェクトにより、空きスペースがたくさんあると思われる任意の領域を選択し、生存オブジェクトを別の(はるかに小さい)領域のセットに移動して圧縮します。違いは、G1がこのGCで、さらにアプリケーションで使用するために解放したものです。

一般的なケースでは、オブジェクトがリージョンをまたいではいけません。典型的なJavaオブジェクトは数十バイトの範囲なので、利用可能なリージョンサイズの範囲(1MBから32MBまで)は、通常は問題になりません。典型的なJavaオブジェクトのサイズは数十バイトなので、利用可能なリージョンサイズの範囲(1MBから32MB)は通常問題ありません。一般的でないオブジェクトがリージョンの半分以上の大きさである場合、G1はそのオブジェクトのためだけに連続した領域のセットをold generationで予約します。

Allocation destination based on region size

上の図は、Javaのヒープを下部の領域とし、これらの空き領域(白い部分)に割り当てられようとしているサイズの異なる数種類のオブジェクトを示しています。Aのような小さなオブジェクトは、領域内のどの位置に割り当てられても構いませんが、Bのような領域の半分以上の大きさの(巨大な)オブジェクト(Humongous object)は、領域内の独立した単一のオブジェクトとして割り当てられます。同様に、リージョンより大きいCタイプのHumongous objectは、リージョンの連続したセットにしか割り当てられません。

Humongous objectの割り当てに関する実装上の選択は、アプリケーションが多くのラージオブジェクトを使用する場合に問題を引き起こす可能性があります。

  • リージョン終了時のフラグメント(断片化)や無駄
    残念ながら実際には、これらのラージオブジェクトのペイロードデータは2^nサイズであることが一般的です。HotSpotオブジェクト・ヘッダを追加すると、オブジェクトの合計サイズがリージョン・サイズを超えてしまい、ほとんど何もないリージョンが無駄になってしまうことが多々あります。例えば、完全なJavaオブジェクトが1つのリージョンよりも1バイト大きくても、ヒープ上の2つのリージョンを占有してしまいます。下図は、ペイロードが2^nバイトのオブジェクトDの例です。この場合、Humongous objectの最後の無駄も、それに比例して非常に大きくなります。

Problem with header of 2^n aligned objects

  • Humongous objectは、配置のために連続したリージョンを必要とします。これは特に、ヒープに他のHumongous objectが既に散らばっている場合に問題になります。G1は効率を考えてそうしたオブジェクトを動かさないので、G1は新しい割り当てのためのスペースを全く見つけられない可能性があります。このような状況の例としては、「Javaヒープの領域は様々な場所で埋め尽くされており、連続した空き領域はほとんどない」という以下のようなものが考えられます。

Problem with region fragmentation

Javaのヒープにまだ多くの空き容量があり、Humongous objectを大量に使用しているにもかかわらず、フルGCや、一般的にGC発生が多いと感じる場合は、これらのフラグメンテーションの問題が原因かもしれません。ヒープ上のHumongous objectは、通常ポーズ時間に影響を与えないにせよ、GCの頻度が増えます。

アプリケーションの変更(例えば、ヒューリスティックのサイズ変更でその値を2倍に拡大したときに、初期の配列サイズを要素数16個ではなく15個で開始する)の他に、humongous objectのeager reclaimのような技術は、ヒープ全体のliveness analysis(すなわち、コンカレント・マーキングまたはフルGC)を使わずにいくつかのhumongous objectを回収することによって、状況を改善します。

[JDK-8048179] Early reclaim of large objects that are referenced by a few objects
https://bugs.openjdk.java.net/browse/JDK-8048179

G1GC Tuning GuideではJavaヒープサイズの増加もしくはリージョンサイズの増加を提示しています。これは利用可能なリージョンの個数を増やす、もしくはこうした問題を生じさせるhumongous objectの量を減らすことにつながります。

Humongous Object Fragmentation
https://docs.oracle.com/en/java/javase/17/gctuning/garbage-first-garbage-collector-tuning.html#GUID-2428DA90-B93D-48E6-B336-A849ADF1C552

これらのリージョンサイズはこれまで32MBに制限されていましたが、これはいくつかの実装上の選択ならびに記憶セットの制限によるものでした。そしてこの制限はJDK-8017163で先頃解除されました。

[JDK-8017163] G1: Refactor remembered sets
https://bugs.openjdk.java.net/browse/JDK-8017163

思い出していただきたいのですが、あるリージョンXの記憶セットには、他のすべてのリージョンからそのリージョンXへの参照のおおよその位置が格納されています。これらの位置は、ガベージコレクションの際に移動したオブジェクトの参照を更新するために必要です。このFOSDEM 2018のプレゼンテーションにおけるFaster Card Scanningの章で、この原理を詳しく説明しています。

G1 – Not Never Done!
https://archive.fosdem.org/2018/schedule/event/g1/

言及されている実装の選択は、特定の記憶セットのカードコンテナに格納できる値の可能な範囲に関連しています。記憶セットコンテナは、特定のリージョンに対して、必要な参照を含むカードを保存します。

Remembered Set Containers

上図は、リージョンAとそれに対応する記憶セットRS(A)を表しています。Aへの参照を持つすべてのリージョンに対して、記憶セットはカード(リージョン内の破線の箱)をそのような記憶された集合のコンテナに格納します。様々な特性を持つ異なるコンテナがあり、異なる色のボックスで描かれています。これらのコンテナの1つであるG1は、16ビット整数を使用して、カードのインデックスを格納し、スペースを節約しています。しかし、これでは領域の最大サイズが制限されてしまいます。

カードのサイズは現在512(=2^9)バイトに固定されているので、2^16 * 2^9 = 2^25 = 32MBが最大領域サイズとなります。

さらに、JDK-1017163以前の記憶されたセットの実装では、Javaヒープ領域と記憶されたセットコンテナの1対1のマッピングが必要でした。つまり、1つのリージョンには1つのコンテナしか存在しない、ということです。

The Change

では、JDK-8275056は、ヒープ領域のサイズ制限を克服するために何をするのでしょうか?

[JDK-8275056] Virtualize G1CardSet containers over heap region
https://bugs.openjdk.java.net/browse/JDK-8275056

新しい記憶セットの実装では、Javaのヒープ領域と記憶セットの1対1のマッピングを必要としません。この変更は、Javaヒープ領域と記憶セット・コンテナの1:nのマッピングを実装しています。1つのヒープ領域を複数のカード領域(指定されたカードセットコンテナがカバーする領域)でカバーするようになりました。

Card regions

上の図は、1つの領域が32MBよりも大きいと仮定したセットアップの例です。Aの左側のヒープ領域が2つのカード領域に分割され、それぞれが記憶セット内に記憶セットのコンテナを持っています。

この変更によりヒープ領域のサイズを512MBにまで構成可能になりました。これは任意の制限値ですが、最大級のhumongous objectやJavaヒープであっても、前述の断片化の問題を回避するのに十分な柔軟性が得られるはずです。G1が目指す「最適」なリージョン数である2048では、この変更によりJavaヒープが1TBを超える必要があります。

Impact Discussion

エルゴノミクスから、選択できる最大のリージョン・サイズは32MBに制限されます。エンド・ユーザー(つまり読者)は、より大きなヒープのヒープ・リージョン・サイズを手動で上書きする必要があります。その理由は、機能しないということではなく、ヒープ領域サイズは、自動的に拡張・補間される他のいくつかのヒューリスティック(マジックナンバー)に影響を与えるからです。G1では32MBまでの領域を使用した経験と性能データがたくさんありますが、それ以上のヒープ領域サイズのデータはあまりありません。

考えられる一つの問題は、G1がextraリージョンにあるオブジェクトをhumongousとして割り当てた場合と、young generationにあるオブジェクトを通常のオブジェクトとして割り当てた場合の前述のしきい値です。退避時には、1つのスレッドが単独でオブジェクトのデータを移動させるため、ヒープ領域のサイズが大きくなるごとに、オブジェクトをコピーするそのシングルスレッドの動作が、ガベージコレクションの一時停止全体のボトルネックになる危険性があります。

これに対して、TLAB(スレッドローカル割り当てバッファ)が現在ではより大きくなることで、Javaアプリケーションのオブジェクト割り当て時の潜在的な競合を減らし、アプリケーションの高速化に寄与する可能性があります。また、リージョンごとに管理されている一部の内部データ構造により、全体のメモリ使用量が減り、より多くのデータをキャッシュに保持できるので、より高速なアクセスが可能になる可能性があります。通常の局所性を仮定した場合、記憶セットが小さくなる可能性があります。それは記録対象のクロスリージョン参照が減り、利用メモリが減り、退避領域への参照探索に要する時間が減り、ポーズ時間が減るためです。

適切なサイズのヒープ上でより大きなヒープ領域サイズを使用した我々や他の研究者の初期の結果は、いくつかの有望な結果を示していますが、128MBと同等以上のヒープ領域サイズのテストでは効果が薄れていることにも気付きました。

[JDK-8272773] Configurable card table card size
https://bugs.openjdk.java.net/browse/JDK-8272773

このエルゴノミクス・ヒューリスティックの制限は意図的なものです。ユーザーが必要な場合にのみ、意図的に大きなヒープ領域サイズを試していただきたいと考え、この変更がアプリケーションに与える影響を徹底的にテストすることを前提としています。ヒープ領域のサイズを調整した経験をお聞かせいただければ幸いです。コメントは著者、hotspot-gc-use at openjdk.java.net または hotspot-gc-dev at openjdk.java.net までお寄せください。

コメントを残す

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

WordPress.com ロゴ

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

Twitter 画像

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

Facebook の写真

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

%s と連携中