web-dev-qa-db-ja.com

Java:メモリ効率の良いByteArrayOutputStream

ディスクに40MBのファイルがあり、バイト配列を使用してメモリに「マップ」する必要があります。

最初は、ファイルをByteArrayOutputStreamに書き込むのが最善の方法だと思いましたが、コピー操作中のある時点で約160MBのヒープ領域が必要であることがわかりました。

RAMのファイルサイズの3倍を使わずにこれを行うより良い方法を誰かが知っていますか?

更新:ご回答ありがとうございます。メモリの消費を少し減らして、ByteArrayOutputStreamの初期サイズを元のファイルサイズよりも少し大きくできることに気づきました(コードで正確なサイズを使用すると、再割り当てが強制され、理由を確認できます)。

もう1つのハイメモリスポットがあります。ByteArrayOutputStream.toByteArrayでbyte []を取得したときです。ソースコードを見ると、配列のクローンが作成されていることがわかります。

public synchronized byte toByteArray()[] {
    return Arrays.copyOf(buf, count);
}

ByteArrayOutputStreamを拡張してこのメ​​ソッドを書き直すだけで、元の配列を直接返すことができると思います。ストリームとバイト配列が2回以上使用されない場合、ここに潜在的な危険はありますか?

16
user683887

MappedByteBuffer はあなたが探しているものかもしれません。

しかし、メモリ内のファイルを読み取るためにRAM)を必要とすることに驚いています。適切な容量でByteArrayOutputStreamを作成しましたか?まだ作成していない場合、ストリーム40 MBの終わりに近づくと、新しいバイト配列を割り当てることができます。これは、たとえば、39MBのフルバッファーと2倍のサイズの新しいバッファーがあることを意味します。ストリームに適切な容量がある場合、再割り当て(高速)はなく、メモリの浪費もありません。

13
JB Nizet

コンストラクタで適切なサイズを指定する限り、ByteArrayOutputStreamは問題ありません。 toByteArrayを呼び出してもコピーは作成されますが、それはtemporaryのみです。あなたは本当にメモリを気にしますか簡単にたくさん上がりますか?

または、最初のサイズがわかっている場合は、バイト配列を作成し、FileInputStreamからそのバッファーに繰り返し読み取り、すべてのデータを取得できます。

10
Jon Skeet

ファイルをメモリにmapしたい場合は、 FileChannel が適切なメカニズムです。

ファイルを単純な_byte[]_に読み込むだけであり(その配列を変更してファイルに反映する必要がない場合)、適切なサイズの_byte[]_に読み込むだけです。通常から FileInputStream で十分です。

Guava には Files.toByteArray() があります。

5
Joachim Sauer

ByteArrayOutputStreamのバッファ拡張動作の説明については、 この答え を参照してください。

あなたの質問に答えて、ByteArrayOutputStreamを拡張しても安全です。あなたの状況では、おそらく追加の最大割り当てが16MBに制限されるように、書き込みメソッドをオーバーライドする方が良いでしょう。 toByteArrayをオーバーライドして、保護されたbuf []メンバーを公開しないでください。これは、ストリームがバッファではないためです。ストリームは、位置ポインターと境界保護を備えたバッファーです。したがって、クラスの外部からバッファにアクセスして操作することは危険です。

3
Derek Bennett

...しかし、コピー操作中のある時点で、約160MBのヒープ領域が必要になることがわかりました

ヒープの使用量を正しく測定しているのではないかと疑っていますが、これは非常に驚くべきことです。

コードが次のようなものであるとしましょう:

_BufferedInputStream bis = new BufferedInputStream(
        new FileInputStream("somefile"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();  /* no hint !! */

int b;
while ((b = bis.read()) != -1) {
    baos.write((byte) b);
}
byte[] stuff = baos.toByteArray();
_

ByteArrayOutputStreamがバッファを管理する方法は、初期サイズを割り当て、(少なくとも)バッファがいっぱいになるとバッファを2倍にすることです。したがって、最悪の場合、baosは最大80Mbのバッファを使用して40Mbのファイルを保持する可能性があります。

最後のステップでは、正確にbaos.size()バイトの新しい配列を割り当てて、バッファの内容を保持します。それは40Mbです。したがって、実際に使用されているメモリのピーク量は120Mbになります。

では、これらの余分な40Mbはどこで使用されているのでしょうか。おそらくそうではなく、実際には、ヒープオブジェクトの合計サイズを報告しているのではなく、到達可能なオブジェクトが占有しているメモリの量を報告しているのではないでしょうか。


それで、解決策は何ですか?

  1. メモリマップドバッファを使用できます。

  2. ByteArrayOutputStreamを割り当てるときに、サイズのヒントを与えることができます。例えば.

    _ ByteArrayOutputStream baos = ByteArrayOutputStream(file.size());
    _
  3. ByteArrayOutputStreamを完全に省略して、バイト配列に直接読み取ることができます。

    _ byte[] buffer = new byte[file.size()];
     FileInputStream fis = new FileInputStream(file);
     int nosRead = fis.read(buffer);
     /* check that nosRead == buffer.length and repeat if necessary */
    _

オプション1と2の両方で、40Mbファイルの読み取り中のピークメモリ使用量は40Mbになるはずです。つまり、無駄なスペースはありません。


コードを投稿し、メモリ使用量を測定するための方法論を説明すると役立ちます。


ByteArrayOutputStreamを拡張してこのメ​​ソッドを書き直すだけで、元の配列を直接返すことができると思います。ストリームとバイト配列が2回以上使用されない場合、ここに潜在的な危険はありますか?

潜在的な危険は、あなたの仮定が正しくない、または誰かが意図せずにコードを変更したためにbecomeが正しくないことです...

2
Stephen C

ByteArrayOutputStreamを拡張してこのメ​​ソッドを書き直すだけで、元の配列を直接返すことができると思います。ストリームとバイト配列が2回以上使用されない場合、ここに潜在的な危険はありますか?

既存のメソッドの指定された動作を変更するべきではありませんが、新しいメソッドを追加することは完全に問題ありません。ここに実装があります:

_/** Subclasses ByteArrayOutputStream to give access to the internal raw buffer. */
public class ByteArrayOutputStream2 extends Java.io.ByteArrayOutputStream {
    public ByteArrayOutputStream2() { super(); }
    public ByteArrayOutputStream2(int size) { super(size); }

    /** Returns the internal buffer of this ByteArrayOutputStream, without copying. */
    public synchronized byte[] buf() {
        return this.buf;
    }
}
_

anyByteArrayOutputStreamからバッファを取得する別の方法でありながらハックな方法は、その writeTo(OutputStream) という事実を使用することです=メソッドは、提供されたOutputStreamにバッファを直接渡します。

_/**
 * Returns the internal raw buffer of a ByteArrayOutputStream, without copying.
 */
public static byte[] getBuffer(ByteArrayOutputStream bout) {
    final byte[][] result = new byte[1][];
    try {
        bout.writeTo(new OutputStream() {
            @Override
            public void write(byte[] buf, int offset, int length) {
                result[0] = buf;
            }

            @Override
            public void write(int b) {}
        });
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    return result[0];
}
_

(これは機能しますが、ByteArrayOutputStreamのサブクラス化がより簡単であることを考えると、それが役立つかどうかはわかりません。)

ただし、残りの質問からは、ファイルの完全な内容の単純な_byte[]_が必要なようです。 Java 7以降、これを行う最も簡単で最速の方法は _Files.readAllBytes_ を呼び出すことです。= Java 6および以下では、 Peter Lawrey's answer のように_DataInputStream.readFully_を使用できます。どちらの方法でも、割り当てられた配列が取得されますonceByteArrayOutputStreamを繰り返し再割り当てすることなく、正しいサイズで。

2
Boann

Google Guava ByteSource は、メモリでのバッファリングに適しているようです。 ByteArrayOutputStreamByteArrayList(Colt Libraryから)などの実装とは異なり、データを巨大なバイト配列にマージするのではなく、すべてのチャンクを個別に格納します。例:

List<ByteSource> result = new ArrayList<>();
try (InputStream source = httpRequest.getInputStream()) {
    byte[] cbuf = new byte[CHUNK_SIZE];
    while (true) {
        int read = source.read(cbuf);
        if (read == -1) {
            break;
        } else {
            result.add(ByteSource.wrap(Arrays.copyOf(cbuf, read)));
        }
    }
}
ByteSource body = ByteSource.concat(result);

ByteSourceは、後でいつでもInputStreamとして読み取ることができます。

InputStream data = body.openBufferedStream();
2
30thh

40 MBのデータがある場合、byte []の作成に40 MB以上かかる理由はわかりません。終了時にbyte []コピーを作成する成長するByteArrayOutputStreamを使用していると思います。

古いファイルを一度に読み取ってみてください。

File file = 
DataInputStream is = new DataInputStream(FileInputStream(file));
byte[] bytes = new byte[(int) file.length()];
is.readFully(bytes);
is.close();

ByteBufferを直接使用できる場合は、MappedByteBufferを使用する方が効率的で、データのコピーを回避できます(またはヒープを多く使用する必要はありません)。

2
Peter Lawrey

... 1GBのファイルを読み取るときと同じ観察でここに来ました:OracleのByteArrayOutputStreamは遅延メモリ管理を持っています。 byte-Arrayは、intなどでインデックスが付けられ、いずれにしても2GBに制限されます。サードパーティに依存していなければ、これが役立つかもしれません:

static public byte[] getBinFileContent(String aFile) 
{
    try
    {
        final int bufLen = 32768;
        final long fs = new File(aFile).length();
        final long maxInt = ((long) 1 << 31) - 1;
        if (fs > maxInt)
        {
            System.err.println("file size out of range");
            return null;
        }
        final byte[] res = new byte[(int) fs];
        final byte[] buffer = new byte[bufLen];
        final InputStream is = new FileInputStream(aFile);
        int n;
        int pos = 0;
        while ((n = is.read(buffer)) > 0)
        {
            System.arraycopy(buffer, 0, res, pos, n);
            pos += n;
        }
        is.close();
        return res;
    }
    catch (final IOException e)
    {
        e.printStackTrace();
        return null;
    }
    catch (final OutOfMemoryError e)
    {
        e.printStackTrace();
        return null;
    }
}
0
Sam Ginrich