Java で OutOfMemoryError: Java heap space
が発生する場合、-Xmx (最大ヒープ サイズ) を増やすほうが良いと思っていました。しかし、実際に -Xmx を減らすことで OutOfMemoryError が解消する経験をしましたのでご紹介します。
-Xmx を減らすと解消する例
対象とするプログラム
以下のようにたくさんのオブジェクトを格納する配列を使用するプログラムで発生しました。
class ObjectArrayAllocator {
public static void main(String[] args) throws NumberFormatException {
int gb = Integer.parseInt(args[0]);
Object[][] objects = new Object[gb][1_000_000_000];
System.out.printf(
"allocated for %d objects%n",
(long) objects.length * objects[0].length
);
}
}
実行してみる
JDK 17 で上記プログラムをコンパイルし、実行してみます。
$ java -Xmx40g ObjectArrayAllocator 7
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at ObjectArrayAllocator.main(ObjectArrayAllocator.java:4)
$ java -Xmx31g ObjectArrayAllocator 7
allocated for 7000000000 objects
-Xmx40g
を指定すると OutOfMemoryError が発生するのに、-Xmx31g
を指定すると正常に実行できることがわかります。
なぜ小さい方が良いのか
実は、オブジェクトのポインタのサイズは、デフォルトではヒープ サイズによって異なります。ヒープが約 32 GB 未満の場合はデフォルトで 32 bits の Compressed Oops が使われます。32 GB を超えるとポインタのサイズが 64 bits に倍増するため、上記の例では -Xmx40g
を指定するとメモリが不足してしまうのです。(ただし、ZGC の場合には常に 64-bit ポインタが使われるそうです。)
32 GB を超えても 32-bit ポインタを使いたい場合
実は、32GB を超えても 32-bit ポインタを使う方法があります。
明示的に Compressed Oops を使いたいことを指定する
まず、-XX:+UseCompressedOops
を指定することで、32-bit ポインタが使える場合には使うように指定します。しかし、これだけだと 32-bit ポインタが使えなかった場合は警告が出るだけで、実行は 64-bit ポインタを使って継続されてしまいます。
$ java -Xmx40g -XX:+UseCompressedOops ObjectArrayAllocator 7
OpenJDK 64-Bit Server VM warning: Max heap size too large for Compressed Oops
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at ObjectArrayAllocator.main(ObjectArrayAllocator.java:4)
さらに、ObjectAlignmentInBytes を指定する
そこで必要になるオプションが -XX:ObjectAlignmentInBytes
です。
オブジェクトはデフォルトで 8 バイト単位で align されるのですが、Compressed Oops は「8 バイト単位なら末尾 3 ビットはいつも 0」という事実を利用してポインタの値を 3 ビット シフトすることで、32 ビットに 35 ビット分の情報を持たせています。
つまり、もし 16 バイト単位で align するならば Compressed Oops は 32 ビットで 36 ビット分の情報を持つことができ、約 64 GB までのヒープ領域を使用することができます。
先ほどのプログラムで、80 億個のオブジェクトの領域を確保するように指定して実行してみます。
$ java -Xmx40g -XX:+UseCompressedOops ObjectArrayAllocator 8
OpenJDK 64-Bit Server VM warning: Max heap size too large for Compressed Oops
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at ObjectArrayAllocator.main(ObjectArrayAllocator.java:4)
$ java -Xmx40g -XX:+UseCompressedOops -XX:ObjectAlignmentInBytes=16 ObjectArrayAllocator 8
allocated for 8000000000 objects
-XX:ObjectAlignmentInBytes
を指定することで、40 GB のヒープでも Compressed Oops を使い続けることができました。
-XX:ObjectAlignmentInBytes
には 8 から 256 までの 2 の冪乗を指定できます。