Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

Java 8リリース以降のG1GCに対する改善

JavaOne 2017のセッションで、Java 8のリリース以降に実施されたG1GCの改善を解説するものがありました。

youtu.be

主要な改善は5つあります。

  • String deduplication (JDK 8u20)
  • Class unloading with concurrent mark (JDK 8u40)
  • Eagerly reclaim humongous regions (JDK 8u60)
  • Adaptive start of concurrent mark (JDK 9)
  • More efficient collections (JDK 9)

String deduplication(String重複除外)

Stringの重複がある場合に、除外して1つを使うようにすることです。

Stringの値は、Stringインスタンスが持つchar配列(Java 9ではbyte配列)が表します。複数のインスタンスでこの値が同一であれば、各インスタンスは値として同じ配列を参照するようにします。この配列はprivate finalなフィールドのため変更されることはなく、複数のインスタンスが同じ配列を使っても何も問題がないからです。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

    /**
     * The value is used for character storage.
...
     */
    @Stable
    private final byte[] value;

GCの間、G1は新たにアロケートされたStringをキューに入れます。youngコレクションのあと、G1は同じ文字列か並列でチェックします。同じであれば、1つの同じ配列を参照するようにします。

ここまでの説明の通り、これはString#intern()とは違います。internはStringオブジェクトそのものに関してですが、deduplicationは内部の配列に関してです。JVMでは内部的に異なるテーブルを使っています。

これでメモリを削減できます。もちろんトレードオフがあります。

  • CPUの使用量が少しだけ上がる
  • youngコレクションが少し長くなる

8u20以降で-XX:+UseStringDeduplicationをつけると試せます。

Class unloading with concurrent mark

全オブジェクトを確認したいあと、生きていないクラスローダがロードしたクラスをアンロードします。G1で全オブジェクトをチェックするタイミングは2つあります。

  • コンカレント・マーキング(トリッキーだがパフォーマンスはよい)
  • フォールバック・フルGC(簡単だが遅い)

8u40でClass unloading with concurrent markはリリースされています。-XX:+ClassUnloadingWithConcurrentMarkで設定できますが、デフォルトで有効です。

Eagerly reclaim humongous regions

G1では各リージョンの半分より大きなサイズのオブジェクトを巨大オブジェクト(humongousオブジェクト)と呼びます。巨大オブジェクトはコピーしません。コストが高いからです。巨大オブジェクトはできる限り早くメモリから回収したいものです。

G1はoldリージョン間のリファレンスは追跡し続けます。なのでもし巨大オブジェクトに対する参照がoldからなければ、youngからのみ参照があるということです。なのでyoungコレクションでG1は巨大オブジェクトへの参照をチェックし、youngからもなければ巨大オブジェクトを回収できるということです。コンカレント・マークの完了を待たなくても、すぐに回収できます。8u60から導入されています。

Adaptive start of concurrent mark

oldリージョンにあるオブジェクトは並列にマーキングされます。ただ、コンカレント・マークが終わるまでoldリージョンは回収されません。そのためoldリージョンでヒープがフルになる前にコンカレントマーキングは完了しなければなりません。マーキングを開始するタイミングはいつがよいのか?ということです。

Java 9以前では-XX:InitiatingHeapOccupancyPercentで設定しました。Java 9ではこれはより適応的、動的な計算で置き換えられました。

  • -XX:InitiatingHeapOccupancyPercentを初期値として使います。
  • 実行時のデータをサンプリングします。
  • データを予測に使います。
  • 予測に基づきセーフティマージンを追加します。

これによりコンカレント・マークの失敗が減り、結果としてフォールバックでのフルGCが減ります。-XX:+G1UseAdaptiveIHOPで設定できますが、デフォルトで有効です。

More efficient collections

以下のような多くの改善です。

  • Decrease Hot Card Cache Contentioon
  • Improved PLAB sizing Remembered set space reduction
  • Parallel clear of the next bitmap
  • Improved concurrent refinement
  • Parallel freeing of collection set
  • Ergonomic thread tuning Parallel pre-touch
  • Parallel promo on failure Improved work distribution
  • Improved clearing of card table Enable TLAB resizing
  • Concurrent mark from roots
  • Cache align and pad from the card cache
  • More concurrent data structures Array-based collection set

Java 9で250以上の拡張と180以上のバグフィックスが入りました。

将来

直近ではJEP 307での並行フォールバック・フルGCとカードスキャンの速度改善です。

JEP 307については、僕が翻訳したInfoQの記事にもあります。

www.infoq.com

JEP 307はG1ガベージコレクタでの問題を解決する。現在のJava 9でのG1のフルGCの実装は単一スレッドでの(シリアルな)アルゴリズムを使っている。 これが意味することはG1が一度でもフルGCにフォールバックしたなら、重いパフォーマンス上の衝撃が待ち受けるということだ。JEP 307の目的はフルGCアルゴリズムを並行化することで、G1のフルGCという起こりそうもないイベントにおいて同一数のスレッドを並列コレクションとして使えるようにするためである。

その後リメンバーセットの再構築を並列で実行することや、エルゴノミクスの改善を予定しています。