ByteArrayOutputStream は、配列を出力ストリームとして扱うクラスです。
なので、このストリームのサイズ上限はシステムの配列サイズの上限に等しい…、はずでした。
しかし、Java SE 8 Update 25 まではそうなっていなかったようです。
[#JDK-8055949] ByteArrayOutputStream capacity should be maximal array size permitted by VM - Java Bug System
例えば、Oracle JDK (64bit版) では配列上限が Integer.MAX_VALUE - 2 (= 2,147,483,645) となっています。
しかし、下記のコードのようにストリームに書き込んでいくと、Java SE 8 Update 25 ではその約半分で OutOfMemoryError が発生します*1。
import java.io.ByteArrayOutputStream; import java.io.IOException; public class Test { public static void main(String[] args) throws IOException { int i = 0; try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { for (; i < Integer.MAX_VALUE; i++) stream.write(0); } catch (Throwable t) { t.printStackTrace(); System.out.println("ERROR: " + i + "bytes"); } } }
java.lang.OutOfMemoryError: Requested array size exceeds VM limit ERROR: 1073741824bytes at java.util.Arrays.copyOf(Unknown Source) at java.io.ByteArrayOutputStream.grow(Unknown Source) at java.io.ByteArrayOutputStream.ensureCapacity(Unknown Source) at java.io.ByteArrayOutputStream.write(Unknown Source) at Test.main(Test.java:9)
問題の原因
ソースコードを確認したところ、バッファがいっぱいになると、バッファサイズを2倍に拡大させていき、最大で Integer.MAX_VALUE (= 0x7fffffff) まで、としていました。
しかし、上記で書いたように Oracle JDK (64bit) だと配列のサイズ上限が Integer.MAX_VALUE - 2 なので、これだと OutOfMemoryError となってしまいます。
// Java SE 8 Update 25, ByteArrayOutputStream.java - L102 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = buf.length; int newCapacity = oldCapacity << 1; if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity < 0) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); newCapacity = Integer.MAX_VALUE; // なんてこったい!! } buf = Arrays.copyOf(buf, newCapacity); }
そのため、バッファサイズを Integer.MAX_VALUE にしようとする直前、つまり Integer.MAX_VALUE の半分が ByteArrayOutputStream のサイズ上限となってしまっていました。
修正方法
そこで、Java 8 Update 31 では、バッファを拡大する際の上限を Integer.MAX_VALUE - 8 に変更していました。
(Integer.MAX_VALUE - 2 ではなく Integer.MAX_VALUE - 8 なのは、VMによって微妙に上限が異なるためです)
そして、それでも足りくなったときに Integer.MAX_VALUE まで拡大を試みるようになっていました。
/** * The maximum size of array to allocate. * Some VMs reserve some header words in an array. * Attempts to allocate larger arrays may result in * OutOfMemoryError: Requested array size exceeds VM limit */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * * @param minCapacity the desired minimum capacity */ private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = buf.length; int newCapacity = oldCapacity << 1; if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); buf = Arrays.copyOf(buf, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
念のため、最初に示したテストコードを Java SE 8 Update 31 で実行すると、上限が変わったことを確認できました。
java.lang.OutOfMemoryError: Requested array size exceeds VM limit ERROR: 2147483639bytes at java.util.Arrays.copyOf(Unknown Source) at java.io.ByteArrayOutputStream.grow(Unknown Source) at java.io.ByteArrayOutputStream.ensureCapacity(Unknown Source) at java.io.ByteArrayOutputStream.write(Unknown Source) at Test.main(Test.java:9)
補足
Oracle JDK (32bit版) では配列上限が Integer.MAX_VALUE / 2 - 3 (= 1,073,741,820) だったので、最近まで気づかれなかったみたいです…。
VM による挙動の違いで起きるバグって厄介ですね!
*1:実行する際は、-Xmx8g を付ける必要があります。