HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

java.io.ByteArrayOutputStream のインスタンスを使いまわしてはいけない

java.io.ByteArrayOutputStream クラスは、各種データをbyte配列にしたい場合に便利なクラスである。
ByteArrayOutputStream (Java 2 Platform SE 5.0)

しかし、このクラスのソースコードを見れば分かるが、このクラスは内部にデータを保持するためのbyte配列を持っている。そして、一旦長さを拡張したbyte配列の長さが短くなることはない。それはreset()メソッドを呼ぼうが、close()メソッドを呼ぼうが変わらない。ByteArrayOutputStreamオブジェクト自体がGCの対象とならない限り、このbyte配列はメモリ上に残り続ける。

ちょっと検証してみる。
たまたまByteArrayOutputStreamクラスのbufフィールド(byte配列)がprotectedなので、検証のために、このbyte配列を直接取得するためのメソッドを追加したクラスを作る。

import java.io.ByteArrayOutputStream;

public class ByteArrayOutputStreamX extends ByteArrayOutputStream {
    
    public ByteArrayOutputStreamX() {
        super();
        // TODO 自動生成されたコンストラクター・スタブ
    }

    public ByteArrayOutputStreamX(int size) {
        super(size);
        // TODO 自動生成されたコンストラクター・スタブ
    }

    public byte[] getRawBuffer() {
        return buf;
    }
}

で、次のコードで検証。

import java.io.IOException;

public class Main {

    /**
     * @param args
     * @throws IOException 
     */
    public static void main(String[] args) throws IOException {
        System.out.println("java.version = " + System.getProperty("java.version"));
        System.out.println("java.vendor  = " + System.getProperty("java.vendor"));
        System.out.println("os.name      = " + System.getProperty("os.name"));
        dump("initial", null);
        ByteArrayOutputStreamX bout = new ByteArrayOutputStreamX(1024);
        dump("gen buf of 1024", bout);
        for (int i = 0; i < 25000000; ++i) {
            bout.write(0);
        }
        dump("write 25MB data", bout);
        bout.flush();
        dump("flush", bout);
        bout.reset();
        dump("reset", bout);
        bout.write(0);
        dump("write 1 byte data", bout);
        bout.close();
        dump("close", bout);
        bout = null;
        dump("set to null", bout);
    }

    private static void dump(String label, ByteArrayOutputStreamX bout) {
        System.out.println("=== " + label + " ===");
        int size = -1;
        int len = -1;
        if (bout != null) {
            size = bout.size();
            len = bout.getRawBuffer().length;
        }
        System.gc();
        System.gc();

        Runtime rt = Runtime.getRuntime();
        long max = rt.maxMemory();
        long total = rt.totalMemory();
        long free = rt.freeMemory();
        long used = total - free;
        System.out.printf("(%10d, %10d) (%10.6f, %10.6f, %10.6f, %10.6f)\n",
                size, len,
                (max / 1024.0 / 1024),
                (total / 1024.0 / 1024),
                (free / 1024.0 / 1024),
                (used / 1024.0 / 1024));
    }
}

実行結果。

java.version = 1.5.0_12
java.vendor  = Sun Microsystems Inc.
os.name      = Windows XP
=== initial ===
(        -1,         -1) ( 63.562500,   1.937500,   1.791611,   0.145889)
=== gen buf of 1024 ===
(         0,       1024) ( 63.562500,   1.937500,   1.738205,   0.199295)
=== write 25MB data ===
(  25000000,   33554432) ( 63.562500,  63.562500,  31.364456,  32.198044)
=== flush ===
(  25000000,   33554432) ( 63.562500,  63.562500,  31.364456,  32.198044)
=== reset ===
(         0,   33554432) ( 63.562500,  63.562500,  31.364456,  32.198044)
=== write 1 byte data ===
(         1,   33554432) ( 63.562500,  63.562500,  31.364456,  32.198044)
=== close ===
(         1,   33554432) ( 63.562500,  63.562500,  31.364456,  32.198044)
=== set to null ===
(        -1,         -1) ( 63.562500,  57.414063,  57.216049,   0.198013)

そりゃあまぁ当然の結果で。
内部的には、書き込まれたデータのサイズ(バイト数)を保持する変数(変数名count)を持っているのだが、

という処理内容なので、byte配列の長さが縮まるわけもなく‥
そういうわけで、ByteArrayOutputStreamオブジェクトは一回限りで使い捨てるのを基本とした方がよいと思われる。(それに反するコードを見つけたためにこんなことを書いているのだが‥)


もちろん、問題点や危険性をきちんと認識した上で使いまわすならば、そしてそのようなコードを書いた人が責任を負い続ける、またはきちんと後続の人たちに引き継がれるならば、(そして願わくば自分がそれに関わらないならば)強く攻め立てる気はないけど‥