はじめに
Javaヒープの中に格納されているJavaオブジェクトはどのような形式になっているのでしょうか。この記事では、Semeru Runtime/OpenJ9のJavaオブジェクトのヘッダーについて説明します。
通常のオブジェクト
下記のようなクラスのオブジェクトを考えます。
class Foo {
static String st;
Object o;
int i;
}
Foo x = new Foo;
64ビットアドレスを持つシステムでは、この変数 x が指すメモリーアドレスには下記のようなレイアウトでデータが書き込まれます。なお、各オブジェクトの先頭アドレスは必ず8の倍数になっています。
0 8 16 24 28
+-------------------+-------------------+-------------------+---------+
| class | lock | o | i |
+-------------------+-------------------+-------------------+---------+
|<- header ->|
ここでnoCRというのはno compressed referencesモード (large heapモード) のことを表します。compressed referencesモードについては前回の記事を参照してください。
レイアウト図上部の0, 8, 16などの数字はオブジェクトの先頭アドレスからのバイト数を示しています。
noCRの場合は先頭16バイトがオブジェクトヘッダーです。
- 先頭8バイトにこのオブジェクトのクラスを指すポインターが格納されます。
- その次の8バイトはロックフィールドです。synchronizedブロック (
synchronized (x) { ... }) などで同期処理を行う際に使用されます。
16バイト目以降はオブジェクトの本体です。オブジェクトのクラスによって長さや内容が変わります。上記の Foo クラスの場合は以下のようになります。
- 16バイト目からは
Object型のフィールドoです。他のオブジェクトを指すオブジェクト参照 (アドレス) がここに格納されます。オブジェクト参照はnoCRでは8バイト (64ビット) 幅です。 - 24バイト目からは
int型のフィールドiです。Javaのint型は4バイト幅と決まっています。intのようなプリミティブ型の場合はその値がここに格納されます。 - staticフィールドである
stはオブジェクトには含まれません。 - オブジェクト全体のサイズは28バイトです。
では、同じ Foo クラスのオブジェクトはcompressed references (CR) モードではどのように表されるでしょうか。
0 4 8 12 16
+---------+---------+---------+---------+
| class | lock | o | i |
+---------+---------+---------+---------+
|<- header ->|
前の記事で説明したとおりCRではJavaヒープのサイズが制限されていて、オブジェクト参照が4バイト (32ビット) 幅になります。Javaヒープが32ビットメモリー空間に収まる場合は4バイト幅の参照の値をそのままオブジェクトのアドレスとして使い、それよりも大きなJavaヒープの場合は参照の値を最大で4ビット左シフトしたものをオブジェクトのアドレスとして使います。
CRでは o が半分の4バイト幅になるだけでなく、オブジェクトヘッダーの class と lock も各4バイト幅になります。プリミティブ型はCR/noCRによって変化しないので i は4バイト幅のままです。
この結果、先頭の8バイトがオブジェクトヘッダー、その後の8バイトがオブジェクト本体となり、オブジェクト全体のサイズは16バイトになります。
配列オブジェクト
次は配列オブジェクトです。下記のような short と Foo の配列を考えます。
short[] s = new short[4];
Foo[] obj = new Foo[3];
CRでのメモリーレイアウトはこのようになります。
0 4 8 10 12 14 16
+---------+---------+----+----+----+----+
| class | size |s[0]|s[1]|s[2]|s[3]|
+---------+---------+----+----+----+----+
|<- header ->|
0 4 8 12 16 20
+---------+---------+---------+---------+---------+
| class | size | obj[0] | obj[1] | obj[2] |
+---------+---------+---------+---------+---------+
|<- header ->|
オブジェクトヘッダーはクラスと配列サイズの2つのフィールドで構成されます。
- この場合のクラスは配列の要素型 (
shortやFoo) のクラスではなく、配列を表すクラス ([Sや[LFoo;のように表記される) です。 - 配列サイズには配列の要素数が格納されています。バイト数ではありません。
同じ例のnoCRの場合を見てみます。
0 8 12 16 18 20 22 24
+-------------------+---------+---------+----+----+----+----+
| class | size | padding |s[0]|s[1]|s[2]|s[3]|
+-------------------+---------+---------+----+----+----+----+
|<- header ->|
0 8 12 16 24 32 40
+-------------------+---------+---------+-------------------+-------------------+-------------------+
| class | size | padding | obj[0] | obj[1] | obj[2] |
+-------------------+---------+---------+-------------------+-------------------+-------------------+
|<- header ->|
配列サイズのフィールドはnoCRでも4バイト幅で変化しませんが、その後ろには新たにパディングフィールドが加わっています。これは、オブジェクト本体の先頭を8の倍数のアドレスに置きたいために挿入されたもので、機能的には何にも使われません。
Balanced GCの場合
Balanced GCは、OpenJ9の持つ様々なGarbage Collection (GC) ポリシーの一つで、GC処理による停止 (Stop-The-World) の最大時間を短くし、Javaヒープの分断化 (fragmentation) を減らすことを目的としています。Balanced GCを使用するにはコマンドラインで -Xgcpolicy:balanced を指定します。
Balanced GCの実装は2025年7月リリースのOpenJ9 v0.53から変更され、offheap allocationという機能が有効になりました。
offheap allocationを行うBalanced GCでは、通常のオブジェクトのメモリーレイアウトに違いはありませんが、配列オブジェクトのレイアウトが隣接 (adjacent) レイアウトと非隣接 (nonadjacent) レイアウトの2種類あります。CRの場合だけを示します。
0 4 8 16 20 24 28
+---------+---------+-------------------+---------+---------+---------+
| class | size | address | obj[0] | obj[1] | obj[2] |
+---------+---------+---------|---------+---------+---------+---------+
| ^
+---------+ (addressフィールド直後にある配列本体のアドレスを指す)
|<- header ->|
大多数の配列オブジェクトは上のようなレイアウトになっています。
オブジェクトヘッダーに8バイト幅のアドレスフィールドが加わっています。このフィールドには、オブジェクト本体の先頭アドレス (上記の例では obj[0] のアドレス) が書き込まれています。
一方、サイズの大きな配列 (デフォルトでは1MB以上) を確保するとこのようになります。
0 4 8 16
+---------+---------+-------------------+
| class | size | address |
+---------+---------+---------|---------+
+-------------> (Javaヒープの外に確保された配列本体のアドレスを指す)
|<- header ->|
アドレスフィールドの後ろの配列本体がなくなってしまいました。offheap allocationでは巨大な配列はJavaヒープとは別の領域に配置されることになっています。オブジェクトヘッダーに追加されたアドレスフィールドはJavaヒープの外のアドレスを指しているため、アドレスフィールドはCRでも8バイト幅になっています。
64ビットArmアーキテクチャ (AArch64) を例にして、上記の配列の要素を読み出す際の命令列を見てみます。
obj[] のn番目の要素を読み出すとして、以下の命令列では
- レジスター
x0がobj[]のオブジェクトヘッダーの先頭アドレス -
w1が読み出す要素の番号 n -
x2が配列本体のアドレス -
w3が読み出されたobj[n]の値
に対応しています。AArch64では汎用レジスター名が x で始まる場合は64ビット幅、w で始まる場合は32ビット幅として扱います。
add x2, x0, #8 ; x2 = x0 + 8 (ヘッダーのサイズ)
ldr w3, [x2, w1, sxtw #2] ; x2 + (w1 << 2) のアドレスから読み出した値を w3 に格納
ldr x2, [x0, #8] ; x0 + 8 のアドレス (オブジェクトヘッダーのアドレスフィールド) から読み出した値を x2 に格納
ldr w0, [x2, w1, sxtw #2] ; x2 + (w1 << 2) のアドレスから読み出した値を w0 に格納
配列本体のアドレスを得る方法がデフォルトのGCポリシーの場合 (add) とBalanced GCの場合 (ldr) で異なっています。Balanced GCの場合は、隣接レイアウトか非隣接レイアウトかを区別することなく同じ命令列で配列の要素にアクセスできることが分かります。
以前のBalanced GCでは配列オブジェクトを表現するためにarrayletと呼ばれる複雑な形式を使用していました。obj[n]の値を読み出すのに配列の要素数による条件分岐が必要になり、命令数が増えて処理も遅かったのです。それを解消するために導入されたのがoffheap allocationと上記の配列レイアウトでした。詳しくはOpenJ9 Blogに説明があります。
まとめ
OpenJ9のオブジェクトのメモリーレイアウトとヘッダーについて説明しました。
文中の図は初めMermaid記法を使って書こうとしたのですが、スキル不足で思うようにできず、コードブロック内のプレーンテキストになりました。