19

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

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

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

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

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

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

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

4

9 に答える 9

13

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

ただし、メモリ内のファイルを読み取るのに非常に多くの RAM が必要であることに驚いています。ByteArrayOutputStream適切な容量でを構築しましたか? そうでない場合、ストリームは 40 MB の終わりに近づいたときに新しいバイト配列を割り当てることができます。つまり、たとえば、39 MB の完全なバッファーと、2 倍のサイズの新しいバッファーがあることを意味します。一方、ストリームに適切な容量がある場合、再割り当ては (より高速に) 行われず、無駄なメモリもありません。

于 2011-08-31T09:50:09.060 に答える
10

ByteArrayOutputStreamコンストラクターで適切なサイズを指定する限り、問題ありません。を呼び出すとコピーが作成されますがtoByteArray、これは一時的なものです。一時的にメモリが大幅に増加することを本当に気にしますか?

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

于 2011-08-31T09:52:33.570 に答える
5

本当にファイルをメモリにマップしたい場合は、aFileChannelが適切なメカニズムです。

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

グアバにはFiles.toByteArray()、あなたのためにすべてを行うものがあります。

于 2011-08-31T09:51:48.660 に答える
3

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

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

/** 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;
    }
}

任意のByteArrayOutputStreamからバッファーを取得する別のハックな方法は、その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 の時点で、これを行う最も簡単で最速の方法は callFiles.readAllBytesです。DataInputStream.readFullyJava 6 以下では、Peter Lawrey の answer のようにを使用できます。どちらの方法でも、ByteArrayOutputStream の再割り当てを繰り返すことなく、正しいサイズで一度割り当てられた配列を取得できます。

于 2014-11-10T18:54:52.353 に答える
3

のバッファ拡張動作の説明については、この回答ByteArrayOutputStreamをお読みください。

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

于 2012-12-05T13:43:27.223 に答える
2

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

古いファイルを一度に読み取るアプローチを試すことができます。

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

MappedByteBuffer を使用するとより効率的であり、ByteBuffer を直接使用できる場合はデータのコピー (またはヒープの使用) を回避できますが、byte[] を使用する必要がある場合はあまり役に立ちません。

于 2011-08-31T10:15:35.533 に答える
2

...しかし、コピー操作中のある瞬間に約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、40Mb のファイルを保持するために最大 80Mb のバッファーを使用する可能性があります。

最後のステップでは、正確にバイトの新しい配列を割り当てて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 を拡張してこのメ​​ソッドを書き直して、元の配列を直接返すことができると考えています。ストリームとバイト配列が複数回使用されない場合、ここに潜在的な危険はありますか?

潜在的な危険は、あなたの仮定が間違っているか、他の誰かがあなたのコードを無意識のうちに変更したために間違っていることです...

于 2011-08-31T10:15:55.877 に答える
2

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

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();
于 2014-09-25T08:48:38.577 に答える