JDK 14のShenandoah GC パート2:並行ルートとクラスアンロード

Red Hat で Solution Architect として OpenJDK を担当している伊藤ちひろ(@chiroito)です。

この記事は、Red Hat Developerのブログ記事、Shenandoah GC in JDK 14, Part 2: Concurrent roots and class unloading | Red Hat Developer の翻訳記事です。


https://developers.redhat.com/sites/default/files/styles/article_feature/public/blog/2020/03/garbage-2729608_1280.jpg?itok=TpxSALSZ

JDK 14のShenandoah GCに関するこのミニシリーズの第1部では、自己修復バリアを取り上げました。この記事では、並行ルート処理と並行クラスアンロードについて説明しています。これらは、GC作業を停止フェーズから並行フェーズに移行することで、GCの停止時間を短縮することを目的としています。

並行ルート処理

いったん並行マーキングが行われると、Shenandoahはマーキングを完了し、退避の準備をする必要があります。 これらは論理的に独立した2つの作業ですが、"Final Mark "という紛らわしい名前の1つの停止期間の下で実行されます。

Shenandoahでは退避自体は並行ですが、停止しなければならないことがいくつかあります。これらは以下の通りです。

  • 弱くないルート(例えば、スレッドスタックや強力なJNIハンドルなど)の事前退避と更新。
  • 弱いルート(例えば、文字列テーブルや弱いJNIハンドル)の事前退避とクリーンアップ。
  • クラスのアンロード。

この作業は停止しながら行われるため、停止時間に影響します。この停止時間を最小限にするために、これらのタスクのほとんどを並行に実行したいと考えています。この方法は、サイズが制限されていないGCルートの場合に特に重要です。

停止中にすべてのGCルートを事前に退避させて更新する必要があるのは、強い不変性を確保するためです。読み出されたり、保存されたりするオブジェクトは、必ずto-spaceにある必要があります。

ここで、重要な注意点があります。GCルートからオブジェクトをロードする際には、ロードリファレンスバリアを採用していません。そのため、アプリケーションはオブジェクトの正しいコピーを見る必要があります。そして、停止期間からアンブロックする前に、退避と更新を実行しなければなりません。この問題には、比較的簡単な解決策があります。それは、関連するGCルートからのロードが「ネイティブLRB」と呼ばれるロードリファレンスバリア(LRB)によって保護されるようにし、それらのルートの実際の更新を並行フェーズに移すことです。

しかし、いわゆる「弱い」ルートは特別です。マーキング中に、あるGCルートにはもう到達できないと判断することがあります。この問題の例として、弱いJNIハンドルがあります。弱いJNIハンドルが(final mark中に)死んだと宣言されたら、それを誤って復活させてはいけません。例えば、死んだと思われるオブジェクトへの参照をヒープに挿入して戻してはいけません。

したがって、(他のすべてのルートと同様に)到達可能な弱いルートを事前に退避させて更新するだけでなく、到達不可能な弱いルートをクリーンアップして、アプリケーションがそれらに触れて復活することができないようにしなければなりません。

このクリーンアップを並行フェーズに移行するには、ネイティブLRBに余分な作業が必要となります。ネイティブLRBは、弱いルートが到達可能かどうかを(マーキングビットマップの指示に従って)確認します。弱いルートに到達できない場合、ネイティブLRBは単にNULLを返します。このようにして、JVMの他の部分に対して、ハンドルがすでにクリーンになっていることを装います。このプロセスにより、既に到達不可能なオブジェクトを誤って再び到達可能にしてしまうことがありません。

擬似的に、ネイティブのLRBは次のようになります。

T native_LRB(T* addr) {
  T obj = *addr; // GCルートからロード
  if (is_reachable(obj)) {
    return LRB(obj);
  } else {
    return NULL;
  }
}

並行クラスアンローディング

final markの停止時のもう一つの大きな項目はクラスアンロードでした。クラスのアンロードはかつてはクラスローダを多用するアプリケーションにとって重要でした。この状況は通常、アプリケーションサーバやその他の大規模なアプリケーション(IDEなど)に当てはまります。しかし、クラスのアンローディングは、無名クラス(それぞれが独自のクラスローダーを持つ)やラムダ(無名クラスに似ている)を使用する場合にも関係してきます。

クラスのアンロードは複雑な手順を必要とします。クラス(というかクラスローダ)が到達可能かどうかをコードが判断する必要があります。このチェックは並行マーキングの際にすでに行われています。すべてのオブジェクト(クラスローダを含む)の到達可能性が確立されると、すべての到達不可能なクラスローダとそのクラス、および補助データ構造がリンク解除され、クリーンアップされる必要があります。それらのクラスに属するコンパイルされたコードもクリーンにする必要があります。

ほとんどの部分で、Shenandoahの実装は、JDK 13でZGC開発者が行った作業に基づいています。この実装では、上述のネイティブバリアが必要です。それに加えて、いわゆる "nmethod entry barrier "も必要です。

通常、停止期間中には、コンパイル済みのすべてのメソッドに埋め込まれているすべての参照を事前に退避させ、更新する必要があります。理想的には、現在実行されている(つまり、スタック上のフレームで到達可能な)メソッドの参照のみを事前退避/更新し、他のメソッドは並行して処理することです。この方法を実現するためには、スレッドがメソッドの実行を開始するというシナリオを処理する必要があります。

nmethod バリアの考え方は、メソッドが呼び出されるたびに実行されるというものです。実行がメソッドに引き渡される前に、GCバリアは特定のことを行うために呼び出されます。Shenandoahでは、メソッドのコードをスキャンして埋め込みオブジェクト(定数)を探し、それらを退避させて更新することを意味します。これは、上記の強力な不変性を確保するためです。生きているnmethodは、最後のマークの停止ですぐ使えるようにされ、並行フェーズ中のGCスレッドか、nmethodが実行されようとしているときにJavaスレッドによって解除されます。

並行のルート処理と並行のクラスアンロードの正味の利点は、final markの停止が短くなることです。そのため、アプリケーションがクラスローダーやJNIハンドルを多用している場合でも、全体的な遅延が改善されます。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。