BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ ニュース JDK 11 の Shenandoah - Red Hat チームとのインタビュー

JDK 11 の Shenandoah - Red Hat チームとのインタビュー

原文(投稿日:2020/10/05)へのリンク

Shenandoahは、Javaアプリケーションを変更することなく迅速に動作させられる低レイテンシのガベージコレクタだ。この機能はJDK 12で最初にアップストリームに導入され、その後、長期サポートのJDK 11にバックポートされた。このバージョンは調査対象の Java ユーザーの 約20~25% が使用している。この変更により、ガベージコレクタをアップストリームの OpenJDK11 リポジトリにバックポートすることで、AzulAdoptiumLibericaなどの Java ベンダーがユーザーに機能を提供できるようになった。

アップストリームのリポジトリに Shenandoah を追加する検討は、すでに機能セットをロックしている Java のバージョンに主要機能を導入するという課題を提起した。単純なバグ修正以上に、この大きな変更はユーザーに何かがうまくいかない機会を生み出した。ユーザーは単に新しい JDK アップデートを期待しているだけだ。このアップデートは基本的には以前のものと同じだが、修正が加えられている。Red Hat のエンジニアリングチームは、Shenandoah を安全に導入するために、これらの課題に着目した。

アップストリームのリポジトリとダウンストリームのディストリビューションの違いはある事実に由来している。それは、OpenJDK が実行可能なバイナリではなく、ソースコードを中心としたコミュニティであるということだ。異なる Java ベンダーは、アップストリームのリポジトリとコードベースを利用して、異なる企業がインストールして使用できる独自のバイナリを構築している。例えば、CentOS のユーザーは、Red Hat のビルドした OpenJDK を一般的に使用しているが、このOpenJDK はすでに長い間 Shenandoah が使える。

InfoQ は、Red Hat の Shenandoah チームの Roman Kennke 氏と Aleksey Shipilev 氏にいくつかの質問を投げかけた。それは、このガベージコレクタがどのようにして作成されたのか、またアップストリームの JDK 11 コードベースへの追加にどのような努力がなされたのかだ。Kennke 氏はプリンシパルソフトウェアエンジニアであり、JEP-304(ガベージコレクタインタフェース)のオーナーだ。Shipilev 氏はプリンシパルソフトウェアエンジニアであり、Java Micro-Harness(JMH)の原著者だ。

InfoQ: Shenandoah の導入の経緯を簡単に教えてください。開発されてどのくらい経つのでしょうか?

Roman Kennke 氏: Shenandoah GC は JDK 8 の頃(2013年)からRed Hatによって開発されています。そして、2017 年の Red Hat Enterprise Linux 7.4 では JDK 8 のビルドで初めてリリースされました。2018 年に JDK 11 がリリースされた際には、RHEL 7.6 から対応する JDK 11 パッケージで Shenandoah もリリースされています。Shenandoah GC は 2018 年にアップストリームの OpenJDK に統合されました。そして、JDK 12 の開始と同時に初めてリリースされています。ここ数ヶ月と数年の間、JDK 13、JDK 14、そして最近では JDK 15 で継続的な改善とリリースが行われてきました。

しかし、多くのユーザーにとっては JDK 11 が最も重要なリリースであることに変わりはありません。それは、多くの JDK ベンダーが JDK 11 を LTS - 長期サポート - リリースとして扱っているためです。特に保守的なユーザーは、JDK 11(通常はJDK 8)を採用するようになったばかりです。それらのユーザーは、Shenandoah GC が提供した利点を利用できませんでした。私たち - JDKチーム - は、ユーザーや JDK ベンダーから Shenandoah GCを JDK 11u のアップストリームに入れたいという多くの要望を受けました(覚えておいてください、Red Hat は 2018 年から独自のビルドで出荷しています)。なので、それをビルドして出荷したいすべてのベンダーが提供できるようにしています。私たちは 2019 年の初めにそのプロセスを開始し、2020 年 7 月以降、Shenandoah GC は JDK 11u のアップストリームソースコードに統合されています。これは 2020 年 10 月中旬にリリースされる予定です。デフォルトではビルドすることはできませんが、ベンダーやユーザーがビルドしたい場合は、スイッチを切り替えるだけでビルドできます (--with-jvm-features=shenandoahgc)。

InfoQ: コミュニティから Shenandoah の採用とフィードバックはどうですか?

Kennke 氏: 興味深いことに(あるいはそうでないかもしれませんが)これまでの採用は、公式の(アップストリームの)JDK 12 以降のリリースからはそれほど多くはありませんでした。JDK 8 や JDK 11 で使用している方からのフィードバックがほとんどです (通常は Red Hat のビルドや派生製品、または独自に提供しているナイトリービルド、場合によっては自作のバイナリのいずれかです)。人々は、それを幅広い目的で使用しているようです。それは、お気に入りのIDEを実行しているところ、巨大な古いアプリケーションサーバを実行しているところ、インメモリデータベースを実行しているところなどがあります。例えば、私は Java 用の IntelliJ と C++ 用の CLion を使って、Shenandoah 上で自分のIDEを実行しています。クラウドサービスをコンテナで動かすために利用しているユーザーも見かけます。これはやや意外かもしれません。なぜなら、Shenandoah の元々のターゲットは100GBの巨大なヒープ JVM インスタンスだったからです。短い一時停止時間は、巨大な設定だけでなく、リソース(CPU、メモリ)が非常に制約されている場合にも有用であることが判明しました。Raspberry Pi 上でデフォルトの GC で JVM を実行してみると、通常では 5ms の GC の一時停止でも 100ms 以上になります。同様の考慮事項は、クラウドでの展開にも適用されます。Shenandoah がこのような環境で非常によく動作するのには、いくつかの理由があります。それは積極的にメモリをアンコミットするように簡単にチューニングできます。(コンパクトモード -XX:ShenandoahGCHeuristics=compact)。これには、比較的少ない補助データ構造を使用しています。これにより、古き良き時代のシリアル GC に匹敵するネイティブメモリフットプリントを実現しています。前述したように、CPUが制約されている場合、短い一時停止時間は短いままです。

InfoQ: アップストリームの JDK 11 のディストリビューションに Shenandoah を導入する際に考えられるリスクは何だったのでしょうか?

Kennke 氏: 私たちは、何かを壊すリスクを冒したことで(当然のことながら)多くの先送りを受けました。特にここでは、Shenandoah GCを無効にした状態でJDK11u を構築する人がいれば、リスクが高いとされています。いくつかの Shenandoah のコードパスはまだ Hotspot の共有コードに「リーク」したままになります。それは Shenandoah を使ってもいない、そしてそれを求めてもいない誰かのために何かを壊すでしょう。だから、このようなことが絶対に起こらないようにする必要がありました。私たちの理想的な目標は、Shenandoah のないビルドのバイナリに Shenandoah の痕跡が一切残らないことでした。私たちはそれを証明したかったのです。このことから、JDK11u に Shenandoah GC を含めることで、Shenandoah なしのビルドを壊すことはないことが証明されました。同様に、Shenandoah 対応のビルドは、別の GC を実行するときも何も壊しません。

InfoQ: その証明を生成するために使用した様々なツールやテクニックは何ですか?

Kennke 氏: 何よりもまず、JDK11のシェナンドーGCを数え切れないほど徹底的にテストしてきました。結局数年前からRed Hat Enterprise Linuxのお客様に出荷していて、夜は寝たいと思っています。

しかし、私たちはこれよりもずっと先に進んでいます。私たちは本質的に、Shenandoah GCのコードをHotspot JVMの残りの部分からできるだけ分離したいと考えていました。この分離のほとんどは、Red Hat と Oracle の共同作業となる JEP 304(Garbage Collector Interface)の作業の一環として、JDK9、10、11 の開発中に達成されました。JDK8 と JDK11 のコードベースを比較して GC 固有のコードを探すと、JDK8 の Hotspot のソースコードの至る所に散りばめられているのがわかります。 そして、JDK11以降の Hotspot では、ほとんどの部分で素晴らしいクリーンなインターフェイスが使用されています。しかし、2018年に JDK11 がリリースされた当初は、このGCのインターフェイスは、Shenandoah GC のすべてをカバーするまでには至っていませんでした。(これがShenandoah GC が JDK12 に遅れることになった主な理由です)。 つまり、Shenandoah 固有のコードを共有コードパスに挿入しなければならないケースがまだいくつかあったということです。

私たちは、このようなインクルードをすべてランタイムチェックで保護するように細心の注意を払っています(例: if (UseShenandoahGC) { ... })。そして、ビルド時にチェック(例: #if INCLUDE_SHENANDOAHGC ...)を行います。これは、ソースコードのパッチを見ると比較的簡単にわかりました。それは Shenandoah がビルド時に無効化されているときに、それは同封されているコードパスをビルドしないだろうということでした。これは、ビルド時にはそのコードパスを実行しないが、ランタイム時にはアクティブではない場合には、そのコードパスを実行しないことを示していました。しかし、それだけでは十分ではありませんでした。私たちは、生成されたバイナリ(libjvm.so)のレベルで証明したかったのです。Shenandoah がビルドから無効化された場合、結果として得られるバイナリはバイト単位で*同じ*になるようにしました。

以前のJDK11u ビルドと、Shenandoah GC を無効にした新しいJDK11u ビルドとの間のバイナリ同一性を証明するために、私は、「clean」(つまり Shenandoah の変更が全くない)と「patch」(JDK11u に Shenandoah を搭載したがビルドからは除外)の両方をビルドするスクリプトを書きました。そして、結果の libjvm.so をバイト単位で比較し、違いがあれば報告します。Shenandoah 関連ではない2つの主要なソースからの違いを発見しました。デバッグ情報の行番号(GDB のようなネイティブコードデバッガで実行しているときにコードをステップスルーするために使用される)とタイムスタンプです(これは、例えば、libjvm.so がビルドされたときに、JVM がクラッシュしたときに java --version や hs_err ファイルで報告するために使用されます)。これらは通常、__LINE__のようなコンパイラの組み込みマクロに含まれています。行番号とタイムスタンプを含まないようにパッチを当てることができました(というか、行番号とタイムスタンプの代わりに常に '0' を含むようにしました)。この後、Shenandoah のコードが Shenandoah 以外のビルドの中に流出している場所をいくつか見つけました。例えば、C2 JIT コンパイラはビルド時にいくつかのコードを生成します。これは JIT バックエンドで使用されます。そして、このコード生成は Shenandoah コードを放出しました。それはきれいにするためにこの活動をした確かに価値がありました。Shenandoah のコードが Shenandoah 以外のビルドに忍び込む、いくつかの隠された道を発見したからです。そして、それらの道は修正されました。

InfoQ: Shenandoah や他の GC と相互作用する JDK ディストリビューション内のファイルにはどのようなものがありますか?そして、それらは何をしますか?

Kenkke 氏: OpenJDK/Hotspot の GC はすべて C++ で書かれています。GC のコードのほとんどは src/hotspot/share/gc/ ディレクトリにあります。そこに、共有サブディレクトリがあることが分かるでしょう。ここには、各 GC と最も重要な GC のインターフェイスの間で共有されているコードが含まれています。言い換えれば、GC が実装することが期待される C++ クラスのセットです(例:CollectedHeap、BarrierSet、最も重要なものをいくつか挙げるとキリがありません)。実装は、名前を付けられたサブディレクトリにあるのが分かります。例えば、Shenandoah GC の src/hotspot/shared/gc/shenandoah です。GC は当然 Hotspot ランタイムの残りの部分と相互作用しますが、C1 と C2 コンパイラと同様にインタプリタのコード生成も行います。後者 2つのGC固有の部分は、各 GC のc1/とc2/のサブディレクトリにあります。例えば、Shenandoah GC のC2 JIT 部分は src/hotspot/share/gc/shenandoah/c2 にあります。また、GC ごとのプラットフォーム固有のコードもあり、最も重要なのはインタプリタ生成コードです。これらのパーツは src/hotspot/$ARCH/gc/$GC の下にあります。例えば Shenandoah GC の X86 固有のコードは src/hotspot/x86/gc/shenandoah の下にあります。

Shipilev 氏:BarrierSets は、GC インタフェースの主要な要素です。 各 GC は時々Java ヒープへのアクセスを望む C++ ランタイムと対話する必要があります。通常はJava ヒープで動作するアプリケーションコードが使用します。C++ ランタイムとの相互作用は、すべての GC が BarrierSet サブクラスを実装する必要があることを意味します。アプリケーションコードには多くの形式があります。テンプレートインタプリタによって実行されるコード、C1によって生成されるコード、C2によって生成されるコードなどです。

テンプレートインタプリタは、高レベルのアセンブリで GC バリアを書き出す必要があります。それはまた、それをプラットフォーム固有のものになります。したがって、ほとんどの GC の実装では、BriarSet、BriarSetC1、BriarSetC2、BriarSetAssembler_x86、BriarSetAssembler_aarch64、...などがあります。また、Shenandoah で Graal をサポートするためにどのような作業が必要かについても説明しています。まだ存在しない BarrierSetGraal です。

InfoQ: パフォーマンスベンチマークはいくつか知られています。SpecJBB、Renaissance、DaCapoなどです。GC チームはどのベンチマークをどのように使っているのか、他には何を使っていますか?

Kennke 氏: 我々は定期的にそれらのベンチマークを実行しています。そして、そこでの回帰の可能性に注意しています。特に特定のパフォーマンス問題に取り組んでいるときには、(悪い)行動の特定の部分を分離し、それを増幅して小さなマイクロベンチマークを作ることがあります。この方が作業は簡単で、そのほとんどがOpenJDK Shenandoah のページでカバーされています。

InfoQ: JDK Mission Control は、最近のトーナメントによる人気投票で優勝しました。Shenandoah との関係は?

Kenkke 氏: Shenandoah は、他のGCと同様に、関連する GC イベントや情報を出力します。 例えば、GC のヒープ占有率、異なるフェーズの GC イベントなどです。

InfoQ:GC はどのようにオブジェクトを識別するのですか?System.identityHashcode のようなポインタを活用しているのか、それとも何か他の何かを活用していますか?

Kennke 氏: GC はゴミオブジェクトの片付けだけに責任があるわけではありません。ガベージコレクタは誤記です。これは、「メモリマネージャ」と呼ばれるべきです。Java ヒープは基本的に GC の完全な制御下にあり、GC はオブジェクトの割り当て*と*解放を扱います。(Aleksey 氏: なぜならばこれは「Epsilon GC」が存在します)。トレーシング GC は、グラフトを縦走することでオブジェクトを見つけます。いわゆる GC-roots(スレッドスタック上の参照変数など)から開始し、それらの「初期」オブジェクトから、すべてのライブオブジェクトを見つけるために、オブジェクトグラフ全体を通して参照を追跡します。

これは、三色マーキングのようなテクニックで効果を発揮します。もっと詳しく知りたい方には、ぜひとも偉大なるGarbage Collection Handbookをお勧めします。

InfoQ:Valhalla のような今後のプロジェクトにはどのような影響があると考えていますか?

Shipilev 氏: ここで言えるのは、GC はオブジェクト単位で動いているということだと思います。そして、多くのオーバーヘッドは、GC が作業する必要のあるオブジェクトの数に依存しています。例えば、マーキングには、生きているオブジェクトをすべて訪問(そして記録)することが含まれています。インライン型では、インライン型の内容を埋め込むようになったので、オブジェクトが粗くなり、GC の作業が楽になりました。その上、インライン型はオブジェクトの密度を高めます。そのため、オブジェクトを移動させる作業が楽になります。そして、割り当て量が下がり、GCの発生頻度が下がるなどします。インライン型は、並行GCの改善をうまく補完します。インライン型は、GC のためのガベージや処理作業を少なくします。 そして、同時進行の GC は、アプリケーションを停止することなく、残りの部分を処理します。

 

この記事に星をつける

おすすめ度
スタイル

BT