はじめに
本稿ではSemeru Runtime/OpenJ9のcompressed references (圧縮参照)という機能について説明します。
Compressed references
64ビットプラットフォーム用のSemeru Runtimeではcompressed referencesモードがデフォルトで有効になっています1。
64ビットシステムではメモリーのアドレスを表すのに64ビット (8バイト) が必要ですが、Javaヒープサイズを制限することによって64ビットアドレスのうち32ビットだけをメモリー上に保持して無駄を減らすことができるというものです。
例として下記のようなクラスを考えます。
class Foo {
String s;
int n;
}
compressed referencesモードがない64ビットプラットフォームの場合、Fooのオブジェクトの本体は64ビット幅の参照が1つ(s)と32ビット幅の整数型1つ(n)で計96ビット (12バイト) の大きさになります。compressed referencesモードではsの参照が32ビット幅になるのでオブジェクト本体のサイズは32+32=64ビット (8バイト) になります。
実際には、クラス情報などを格納しているオブジェクトヘッダーのサイズをオブジェクト本体のサイズに足したものがオブジェクト全体の大きさになります。
compressed referencesモードを無効にした場合 (large heapモード) と比べた場合のメリット・デメリットは下記のとおりです。
メリット:
- オブジェクト内に格納する他のオブジェクトへの参照が小さく済むので、同じオブジェクトを表す際に必要なバイト数が減る
- その結果として同じサイズのJavaヒープ空間により多くのオブジェクトを格納することができ、GCの回数が減る
-clone()メソッドやGCでオブジェクトをコピーする際にコピー量が少なくなる
- CPUのキャッシュメモリーに収まるオブジェクトが増え、キャッシュミスが減る
デメリット:
- Javaヒープサイズが制限される
- 64ビット幅のアドレス←→32ビット幅でメモリーに格納される参照の間で変換するオーバーヘッドがある (後述)
差し引きで、一般的にはcompressed referencesモードのほうが性能は高くなることが多いと考えられます。
compressed referencesモードでは、Javaヒープのサイズは最大で57GBまでに制限されます2。なんだか中途半端な数字ですが、どういう計算で57GBが境界値に決まったのかは調べた範囲では分かりませんでした。
-Xmxオプションで57GBを超えるJavaヒープサイズを指定した場合には自動的にcompressed referencesモードが無効化されます。一方、57GB以下のJavaヒープサイズでも-Xnocompressedrefsオプションを指定することで明示的にcompressed referencesモードを無効化することができます。
実行例
OpenJ9が生成するjavacoreファイルを使って、どのようにJavaヒープ用のメモリー空間が確保されるか見てみます。下記ではAArch64 Ubuntu上でSemeru Runtime (17.0.17+10-openj9-0.56.0)を使用しました。
ヒープサイズ2GB (1)
まずは、最大Javaヒープサイズとして2GBを指定(-Xmx2G)した場合です。下記のコマンドを実行するとJavaヒープなどのサイズ情報とOpenJ9のバージョン情報を出力した後、javacoreファイルを生成してjavaプロセスを終了します。
$ jdk-17.0.17+10/bin/java -Xdump:java:events=vmstop,request=exclusive+preempt -Xmx2G -verbose:sizes -version
(中略)
-Xmns2M initial new space size
-Xmnx512M maximum new space size
-Xms8M initial memory size
-Xmos6M initial old space size
-Xmox2046M maximum old space size
-Xmx2G memory maximum
(中略)
openjdk version "17.0.17" 2025-10-21
IBM Semeru Runtime Open Edition 17.0.17.0 (build 17.0.17+10)
Eclipse OpenJ9 VM 17.0.17.0 (build 17.0.17+10-openj9-0.56.0, JRE 17 Linux aarch64-64-Bit Compressed References 20251021_1119 (JIT enabled, AOT enabled)
(中略)
JVMDUMP039I Processing dump event "vmstop", detail "#0000000000000000" at 2026/01/17 19:25:46 - please wait.
JVMDUMP032I JVM requested Java dump using '/home/ubuntu/openj9/javacore.20260117.192546.466320.0001.txt' in response to an event
JVMDUMP010I Java dump written to /home/ubuntu/openj9/javacore.20260117.192546.466320.0001.txt
JVMDUMP056I Processed dump event "vmstop", detail "#0000000000000000" at 2026/01/17 19:25:46 (0.021 seconds).
java -versionの出力の中にCompressed Referencesの文字列が見えます。compressed referencesモードで実行されていることを示しています。
生成されたjavacoreファイルの1STHEAPTYPEの部分を見てみます。
1STHEAPTYPE Object Memory
NULL id start end size space/region
1STHEAPSPACE 0x0000E62E7409ADF0 -- -- -- Generational
1STHEAPREGION 0x0000E62E7409B8F0 0x0000000080000000 0x0000000080600000 0x0000000000600000 Generational/Tenured Region
1STHEAPREGION 0x0000E62E7409B350 0x00000000FFE00000 0x00000000FFF00000 0x0000000000100000 Generational/Nursery Region
1STHEAPREGION 0x0000E62E7409AEC0 0x00000000FFF00000 0x0000000100000000 0x0000000000100000 Generational/Nursery Region
右端にGenerational/Tenured Regionと書かれているのが世代別GCのold space、Generational/Nursery Regionと書かれているのがnew spaceに対応します。
old spaceのサイズは0x60_0000 = 6MB、new spaceのサイズは0x10_0000 = 1MBが2つで計2MBとなっていて、java -verbose:sizesによる出力の中の-Xmos (initial old space size)と-Xmns (initial new space size) の値に一致しています。
(本稿では4桁ごとに_を入れて桁数の大きな16進数を読みやすくしています)
ヒープサイズ2GB (2)
続いて、-Xmsオプションで初期ヒープサイズも2GBに指定してみます。
$ jdk-17.0.17+10/bin/java -Xdump:java:events=vmstop,request=exclusive+preempt -Xms2G -Xmx2G -verbose:sizes -version
(中略)
-Xmns512M initial new space size
-Xmnx512M maximum new space size
-Xms2G initial memory size
-Xmos1536M initial old space size
-Xmox1536M maximum old space size
-Xmx2G memory maximum
(後略)
old spaceのサイズは1536MB (1.5GB)、new spaceのサイズは512MB (0.5GB)、合わせてJavaヒープサイズは2GBに設定されました。
1STHEAPTYPE Object Memory
NULL id start end size space/region
1STHEAPSPACE 0x0000E826D009B070 -- -- -- Generational
1STHEAPREGION 0x0000E826D009BB70 0x0000000080000000 0x00000000E0000000 0x0000000060000000 Generational/Tenured Region
1STHEAPREGION 0x0000E826D009B5D0 0x00000000E0000000 0x00000000F0000000 0x0000000010000000 Generational/Nursery Region
1STHEAPREGION 0x0000E826D009B140 0x00000000F0000000 0x0000000100000000 0x0000000010000000 Generational/Nursery Region
javacoreファイルで見ても、Tenured Regionが0x6000_0000 (=1536MB)、Nursery Regionが0x1000_0000 (=256MB) 2つで512MB、合わせて2GBになっています。
Javaヒープに割り当てられたアドレス空間は0x8000_0000~0x1_0000_0000の2GBで、メモリーの最初の4GB空間に収まっていることが分かります。
この場合、メモリーに格納された32ビット幅の参照の値をそのままオブジェクトのアドレスとして使用します (64ビットアドレスの上位32ビットは全桁ゼロとして扱う)。
ヒープサイズ57GB
では、compressed referencesモードの上限である57GBのJavaヒープを要求するとどうなるでしょうか。
$ jdk-17.0.17+10/bin/java -Xdump:java:events=vmstop,request=exclusive+preempt -Xms57G -Xmx57G -verbose:sizes -version
1STHEAPTYPE Object Memory
NULL id start end size space/region
1STHEAPSPACE 0x0000E3A54009BBB0 -- -- -- Generational
1STHEAPREGION 0x0000E3A54009C6B0 0x00000001C0000000 0x0000000C70000000 0x0000000AB0000000 Generational/Tenured Region
1STHEAPREGION 0x0000E3A54009C110 0x0000000C70000000 0x0000000E38000000 0x00000001C8000000 Generational/Nursery Region
1STHEAPREGION 0x0000E3A54009BC80 0x0000000E38000000 0x0000001000000000 0x00000001C8000000 Generational/Nursery Region
Javaヒープのアドレス空間は0x1_C000_0000~0x10_0000_0000の0xE_4000_0000バイト=57GBとなりました。このアドレス空間を表すには36ビット幅が必要ですが、前述のとおり、メモリー上に格納されるオブジェクト参照は32ビット幅です。どのようにして32ビット幅を超えたアドレスにアクセスするか。
実はこの場合、メモリーから読み出した32ビット幅の値を4ビット左シフトシフトしてからオブジェクトのアクセスに使います。
メモリーに格納されている参照 (32ビット幅): 0xEF01_2345
↓ 4ビット左シフト
オブジェクトのアドレス (36ビット幅): 0xE_F012_3450
JITコンパイラーによる最適化が効くので、オブジェクトを参照するたびにこの変換処理が発生するわけではありません。
オブジェクトのアドレスをメモリー上に格納する際には、逆に4ビット右シフトして32ビット幅に圧縮する操作が加わります。これが「デメリット」の項で触れた変換のオーバーヘッドです。
なお、左シフトによってオブジェクトのアドレスの下位4ビットが必ず0になってしまうため、オブジェクトは16の倍数のアドレスにしか配置できないという制約を課すことによってビット数の不足を補っています。OpenJ9ではそもそも、4ビットシフトを使用しない場合でもオブジェクトのアドレスは8の倍数 (下位3ビットが0) という制約があります。
理屈上は、5ビット以上のシフト幅を使うようにすればcompressed referencesモードでもっと大きなJavaヒープを確保することも可能ですが、そうするとオブジェクトのアラインメントによってJavaヒープ内に生じる無駄な隙間が増えてしまうので、シフト幅は4ビットまでになっています。
ヒープサイズ58GB
次はcompressed referencesモードの上限を超えて58GBのJavaヒープを要求してみます。
$ jdk-17.0.17+10/bin/java -Xdump:java:events=vmstop,request=exclusive+preempt -Xms58G -Xmx58G -verbose:sizes -version
(中略)
openjdk version "17.0.17" 2025-10-21
IBM Semeru Runtime Open Edition 17.0.17.0 (build 17.0.17+10)
Eclipse OpenJ9 VM 17.0.17.0 (build 17.0.17+10-openj9-0.56.0, JRE 17 Linux aarch64-64-Bit 20251021_1119 (JIT enabled, AOT enabled)
(後略)
java -version の出力から "Compressed References" の文字が消えて、large heapモードになったことが分かります。
1STHEAPTYPE Object Memory
NULL id start end size space/region
1STHEAPSPACE 0x000071FCE40B3B30 -- -- -- Generational
1STHEAPREGION 0x0000FFEEA00A0730 0x0000FFE01EC60000 0x0000FFEAFEC60000 0x0000000AE0000000 Generational/Tenured Region
1STHEAPREGION 0x0000FFEEA00A0190 0x0000FFEAFEC60000 0x0000FFECCEC60000 0x00000001D0000000 Generational/Nursery Region
1STHEAPREGION 0x0000FFEEA009FD00 0x0000FFECCEC60000 0x0000FFEE9EC60000 0x00000001D0000000 Generational/Nursery Region
Javaヒープのアドレス空間は0xFFE0_1EC6_0000~0xFFEE_9EC6_0000の0xE_8000_0000バイト=58GBとなりました。Javaオブジェクトのアドレスに使えるビット数に制約がなくなったので、アドレスの上位32ビットに0でない桁が増えています。
ヒープサイズ2GB (nocompressedrefs)
最後に、Javaヒープサイズは2GBで、large heapモード (-Xnocompressedrefs) を指定した場合を見てみます。
$ jdk-17.0.17+10/bin/java -Xdump:java:events=vmstop,request=exclusive+preempt -Xnocompressedrefs -Xms2G -Xmx2G -verbose:sizes -version
1STHEAPTYPE Object Memory
NULL id start end size space/region
1STHEAPSPACE 0x000073216C0B32B0 -- -- -- Generational
1STHEAPREGION 0x0000E98D7C09FEB0 0x0000E98CFC000000 0x0000E98D5C000000 0x0000000060000000 Generational/Tenured Region
1STHEAPREGION 0x0000E98D7C09F910 0x0000E98D5C000000 0x0000E98D6C000000 0x0000000010000000 Generational/Nursery Region
1STHEAPREGION 0x0000E98D7C09F480 0x0000E98D6C000000 0x0000E98D7C000000 0x0000000010000000 Generational/Nursery Region
compressed referencesモードでは2GBのJavaヒープは最初の4GB空間に配置されましたが、large heapモードでは0xE98C_FC00_0000からの2GBがJavaヒープ用に割り当てられました。
AOTとの関係
OpenJ9のAOT (Ahead-Of-Timeコンパイル) は、JITコンパイラーがJavaメソッドをコンパイルして生成したネイティブコードをSCC (Shared Classes Cache)に保管しておいて、後に起動するJavaプロセスがそれを再利用できるようにするものです。詳しくは以前の記事を参照してください。
compressed referencesモードではJavaオブジェクトをアクセスするためのアドレスをJavaヒープサイズによってシフトしたりしなかったりすると書きました。
アドレスをシフトする命令はAOTのネイティブコードに埋め込まれるため、AOTコンパイルするプロセスとAOTコードを利用するプロセスでシフトするビット数が違っているとAOTコードが正しく動作しません。OpenJ9にはそのような場合のチェックが入っていて、AOTコードが非互換の場合にはAOTコードの利用を停止するようになっています。
以下で試してみます。
まず、SCCを消去します。
$ jdk-17.0.17+10/bin/java -Xshareclasses:destroy
次に、デフォルトのJavaヒープサイズで実行してSCCを作成し、JavaメソッドをAOTコンパイルします。
$ jdk-17.0.17+10/bin/java -version
-Xjit:verboseオプションをつけて再実行します。
$ jdk-17.0.17+10/bin/java -Xjit:verbose,vlog=/tmp/vlog -version
ここで生成されたJIT vlog (verbose log)ファイルを見ると、一部のJavaメソッドについて+ (AOT Load)の文字列があり、初回の実行でSCCに保管されたAOTコードをロードして利用したことが読み取れます。
+ (AOT load) java/lang/Object.<init>()V @ 0000F109E9200580-0000F109E92005E0 Q_SZ=0 Q_SZI=0 QW=1 j9m=0000000000025BC8 bcsz=1 compThreadID=0
+ (cold) jdk/internal/reflect/Reflection.getCallerClass()Ljava/lang/Class; @ 0000F109E92005FC-0000F109E9200728 OrdinaryMethod - Q_SZ=0 Q_SZI=0 QW=1 j9m=0000000000041368 bcsz=2 JNI compThreadID=0 CpuLoad=1%(0%avg) JvmCpu=0%
+ (AOT load) java/lang/String.lengthInternal()I @ 0000F109E9200770-0000F109E920085C Q_SZ=0 Q_SZI=0 QW=1 j9m=0000000000036370 bcsz=25 compThreadID=0
今度は、Javaヒープサイズを変えて再実行してみます。compressed referencesモードで最大の57GBで、アドレスを4ビットシフトします。
$ jdk-17.0.17+10/bin/java -Xmx57G -Xjit:verbose,vlog=/tmp/vlog -version
JIT vlogファイルを確認すると下記のメッセージが出力されています。vlogファイルの続きを見ても今回は+ (AOT load)で始まる行がありません。
#INFO: AOT header validation failed: incompatible compressed pointer shift
メッセージのとおり、AOTヘッダーにはcompressed referencesモードでオブジェクトのアドレスを何ビットシフトする必要があるかという情報が保存されていて、それが実行時の環境と一致しない場合にはAOTを無効化するわけです。
十分にAOTコンパイルしたSCCファイルがあるのにJavaアプリケーションの起動が遅い場合は、上記のようなことが原因でAOTが無効になっているのかもしれません。AOT header validation failedが出ていないかチェックするといいでしょう。compressed referencesモードのシフト量以外の原因でもAOTが無効になる場合があります。
なお、compressed referencesモードとlarge heapモードのSCCファイルは別になっています。
$ ls ~/.cache/javasharedresources/
C290M17F1A64P_sharedcc_ubuntu_G45L00 C290M17F2A64P_sharedcc_ubuntu_G45L00
Java 17の場合を例にすると、ファイル名がC290M17F1で始まっているのがcompressed references用、C290M17F2で始まっているのがlarge heap用のSCCファイルです。SCCのファイル名の形式は将来のSemeru Runtime/OpenJ9では変更になる可能性があります。
互換性
compressed referencesと似たような機能はHotSpot VMにも存在して、-XX:+UseCompressedOops/-XX:-UseCompressedOopsで切り替えられます。
Semeru Runtime/OpenJ9 でも互換性のためにこれらのオプションを指定することができ、-Xcompressedrefs/-Xnocompressedrefsを指定したのと同じ意味になります。
参照
- https://eclipse.dev/openj9/docs/xcompressedrefs/
- https://eclipse.dev/openj9/docs/allocation/#compressed-references