Java SE 8 Update 31 で、ByteArrayOutputStream のサイズ上限が拡大していました

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 を付ける必要があります。