原文はこちら。
The original article was written by Paul Hübner (Software Engineer at Oracle).
https://inside.java/2025/08/27/thesis-gc-agnostic-load-barriers/
本稿で紹介する研究は、Oracle、Uppsala大学、KTHの共同研究プロジェクトの一環として実施されたものです。
Oracle, Uppsala University, and KTH in joint JVM research projects
https://inside.java/2020/06/12/joint-research-projects/
こんにちは、Paulと申します。最近、分散システム向けソフトウェア工学の修士号を取得しました。
Paul Hübner
https://arraying.de/
修士論文では、OracleストックホルムオフィスのHotSpot GCおよびコンパイラ開発チームと共同研究を行いました。本稿では、以下のトピックについて私の研究内容と知見を紹介します。
- ウォームアッププロセスと、Project Leydenによってウォームアッププロセスをどのように改善しようとしているのか
- ロードとは何か、そしてなぜGCバリアが必要になる場合があるのか
- ZGCの実際のバリアアセンブリ命令
- GCに依存しないバリアの構築方法
Warmup
飛行機が離陸速度に達するためにエンジン回転を上昇させるように、HotSpot JVM上のJavaコードは、ウォームアップと呼ばれるプロセスを経て、可能な限り高速に実行を開始しようとします。ウォームアップ中に、アプリケーションを解釈および/またはプロファイリングし、最適化されたマシンコードに変換するJITコンパイルを決定します。これは、コンパイル時にアプリケーションコードを最適化し、実行時のウォームアップを不要とするAOTコンパイル(例:C++)とは対照的です。それでも、HotSpot JVMのように実行時駆動の最適化を組み込むことはできません。
幸い、両立させる方法があります。Project LeydenはAOTコンパイルによるウォームアップ時間短縮を模索しています。
Project Leyden
https://openjdk.org/projects/leyden/
トレーニング実行でアプリケーションを実行し、ウォームアップ済みマシンコードを本番実行時に即時利用可能にします。これにより本番環境でのウォームアップが不要となり、スループットが劇的に向上します。
Problem Statement
現在のAOTコンパイル済みコードの主な欠点は、トレーニング時と本番環境で同一のGCを必要とすることです。異なるGCにはそれぞれ長所・短所があるため、これは実行時の柔軟性を損ないます。
Available Collectors
https://docs.oracle.com/en/java/javase/24/gctuning/available-collectors.html
https://docs.oracle.com/javase/jp/24/gctuning/available-collectors.html
この要件の原因は、メモリ操作の周囲に挿入される管理命令のスニペットであるGCバリアにあります。この管理はGCの健全性を保証するために不可欠で、特に重要なのは、この管理をどこで、いつ、どのように行うかは、メモリ操作とGCに完全に依存する点です。結果として、あるGC向けにコンパイルされたコードは別のGCでは使用できません。
本研究では、フィールドアクセス(例:int foo = point.x; はフィールドxへのアクセスを表す)という、単一のメモリ操作に焦点を当てました。フィールドアクセスはロード (load) と呼ばれ、以降はこの用語を使用します。本研究では、GC非依存のロードバリアを形成するためのタイミングと方法の統一に焦点を当てました。コンピュータサイエンスのほとんどはトレードオフであるため、研究課題はGC非依存バリアを使用するコードのパフォーマンスは、GC固有バリアと比較してどうなのか?というものでした。
Implementation
本論文では、AArch64(私のノートPCのアーキテクチャ)上のSerial、Parallel、G1、Zの各GCの検証に限定しました。対象はC2 JITコンパイラのみです。これは長寿命のマシンコードを生成し、Project Leydenが採用しているためです。本節では、上記4つのGC全てがサポートする唯一の圧縮モードである非圧縮Oops (uncompressed Oops) のみを考慮します。
C2 JIT Compiler
https://openjdk.org/groups/hotspot/docs/HotSpotGlossary.html#C2Compiler
CompressedOops
https://wiki.openjdk.org/display/HotSpot/CompressedOops
Answering when
参照の種類に応じて、ロード中に発生する可能性のある操作は複数存在します。オブジェクトのフィールドが読み取られる場合、これは強参照であり、そのアクセスはstrong loadに分類されます。Javaには弱参照も存在します。弱参照のアクセスはweak loadです。
WeakReference
https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/ref/WeakReference.html
オブジェクトが弱参照のみで参照されている場合、そのオブジェクトはGCの対象です。このしくみはキャッシュに有用で、例えばWeakHashMapで利用されています。
WeakHashMap
https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/util/WeakHashMap.html
現在、(強参照・弱参照を問わず)ロードが発生するたびに、フィールドの参照解除前にロードバリアが発行される可能性があります。これが発生する条件は下表に示す通りです。
| GC | Strong load emits barrier? | Weak load emits barrier? |
|---|---|---|
| Serial | ❌ No | ❌ No |
| Parallel | ❌ No | ❌ No |
| G1 | ❌ No | ✅ Yes |
| Z | ✅ Yes | ✅ Yes |
ここで重要な点は、strong loadとweak loadの両方において、少なくとも1つのGCがバリアを発行するということです。したがって、GCに依存しないロードバリアは、すべてのstrong loadとweak loadでロードバリアを発行しなければなりません。
Answering how
ℹ️このセクションではAArch64アセンブリを掘り下げます。このアセンブリがなぜその動作をするのかを理解する必要はありません。関連するのは命令とそのオペランドのみです。
ハイレベルな考え方は、全てのGC(strong loadとweak loadそれぞれに対して)に対して同一の命令を生成し、初回実行時にオペランドをGC固有の要件に合わせて「パッチ処理」するというものです。HotSpotでコンパイルされたマシンコードはメモリ上に存在し、(AArch64上では)各命令は32ビット幅です。特定のメモリ位置の命令に対してパッチを当てるため、その値を32ビット符号なし整数にキャストし、ビット演算で修正します。ZGCでは既にバリアにこの技術を採用しているため、GC非依存バリアにおけるパッチ手法は新規性がありません。
GC非依存バリアは具体例で説明するのが最適です。ここではstrong loadを例にします。ZGCはstrong loadにロードバリアを必要とする唯一のGCなので、そのロードバリアを非依存型ロードバリアのテンプレートとして使用します。これによりZではそのまま動作します。Serial、Parallel、G1では、実行時命令パッチングを用いて、ロードバリアの命令を何もしないよう変更します。
Zのstrong load barrierは次の通りです。この場合、オブジェクト参照はレジスタ0に格納されたcolored pointerです。
tbz w0, Z_MASK, #0x8
b Z_SLOW_PATH
lsr x0, x0, #16
Z_MASKで識別されるアドレスの一部がゼロの場合、次の命令bはスキップされて実行しません。次の命令ではslow pathに分岐します。これはGC用語で、必要だが遅い管理命令を意味しています。最後にcolored pointerから最下位16ビット(メタデータビット)を除去し、アドレスのみを残します。
Serial、Parallel、G1の各GCでは、このバリアが何もしないことを保証する必要があります。strong loadに対する管理を実行する必要がないからです。これは実際の命令を実行しないように命令のオペランドを修正することで実現します。結果は次の通りです。
tbz wzr, Z_MASK, #0x8
b Z_SLOW_PATH
lsr x0, x0, #0
アドレスレジスタ0をテストするのではなく、最初の命令を変更し、ゼロレジスタwzrの命令を操作するようにします。このレジスタの値は常にゼロです。従って、次の命令bを常に飛び越えて、Zの管理処理は決して実行されません。アドレスはレジスタ0に依然存在し、他のGCはcolored pointerを持たないので、右シフトされる量はゼロに修正され、アドレスは変更されません。結局、実際の処理は何も行われないのです。
まとめると、オペランドを修正することで、strong loadに対するGC非依存性を実現しました。同じ考え方はweak load barrierにも適用可能ですが、ここでは説明を省略します。なお、その場合の命令はG1とZの命令のハイブリッドになります。
Performance Results
Emitting a bunch of extra instructions for GCs that do not need them has got to be detrimental to performance, right? Unsurprisingly, yes. When looking at execution time and P90 latencies, the worst-case regressions are shown in the table below:
不要なGCに対して余分な命令を大量に発行することは、パフォーマンスに悪影響を及ぼすはずですよね?当然ながら、その通りです。実行時間とP90レイテンシを調べ、最悪ケースの性能低下を下表に示します。
| GC | Execution time regression | P90 latency regression |
|---|---|---|
| Serial | 17% | 15% |
| Parallel | 19% | 17% |
| G1 | 17% | 20% |
| Z | 23% | 39% |
これらの結果は必ずしも悪いわけではありません。多くの実験では、パフォーマンスの低下は1桁パーセントに留まりました。さらに、インタプリタコードは一般的にコンパイル済みコードよりも1桁遅いため、最終的には依然として恩恵はあります。
Conclusions and Future Work
GC-agnostic load barriers can be implemented through instruction patching. A trade-off between the ability to choose which GC and runtime performance has to be made when using ahead-of-time compilation. Nonetheless, GC-agnostic load barriers present a viable prototype. To gain a broader understanding of the significant impact of GC-agnostic load barriers, the next step would be to integrate them with GC-agnostic store barriers, as investigated by my colleagues Arveen Emdad and Antón Seoane Ampudia.
GC非依存のロードバリアは命令パッチングによって実装可能です。AOTコンパイルを使用する際には、GCの選択可能性とランタイム性能のトレードオフが生じます。それでもなお、GC非依存型ロードバリアは実用的なプロトタイプを提供します。GC非依存型ロードバリアの重大な影響をより広く理解するためには、同僚のArveen EmdadとAntón Seoane Ampudiaが調査したように、GC非依存型ストアバリアとの統合が次のステップとなります。
ストックホルムオフィスの皆様の指導と支援に感謝します。特に、Erik Österlund、Roberto Castañeda Lozano、Tobias Wrigstad、Daniel Lundénの各氏にはご指導とご支援を、Jesper WilhelmssonにはHotSpotのC2コンパイラとGCの内部動作を探求するこの貴重な機会を提供していただき、深く感謝申し上げます。本投稿へのフィードバックをくださったAna Maria MihalceanuとAntón Seoane Ampudiaに感謝します。
本投稿にご興味をお持ちの方は、より詳細な内容と結果をまとめた私の論文レポートをぜひご覧ください。
Garbage Collector Agnostic Load Barriers: Moving Towards Flexible Ahead-of-Time Java Compilation
https://kth.diva-portal.org/smash/record.jsf?pid=diva2%3A1986672&dswid=-3470