19

我在磁盘中有一个 40MB 的文件,我需要使用字节数组将它“映射”到内存中。

起初,我认为将文件写入 ByteArrayOutputStream 是最好的方法,但我发现在复制操作期间的某个时刻需要大约 160MB 的堆空间。

有人知道在不使用三倍 RAM 文件大小的情况下执行此操作的更好方法吗?

更新:感谢您的回答。我注意到我可以稍微减少内存消耗,告诉 ByteArrayOutputStream 初始大小比原始文件大小大一点(使用我的代码强制重新分配的确切大小,必须检查原因)。

还有另一个高内存点:当我用 ByteArrayOutputStream.toByteArray 取回 byte[] 时。查看它的源代码,我可以看到它正在克隆数组:

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

我想我可以扩展 ByteArrayOutputStream 并重写这个方法,所以直接返回原始数组。考虑到流和字节数组不会被多次使用,这里是否有任何潜在的危险?

4

9 回答 9

13

MappedByteBuffer可能是你正在寻找的。

不过,我很惊讶读取内存中的文件需要这么多 RAM。您是否构建了ByteArrayOutputStream具有适当容量的 如果没有,流可以在接近 40 MB 的末尾时分配一个新的字节数组,这意味着您将拥有一个 39MB 的完整缓冲区和一个两倍大小的新缓冲区。而如果流具有适当的容量,则不会有任何重新分配(更快),也不会浪费内存。

于 2011-08-31T09:50:09.060 回答
10

ByteArrayOutputStream只要您在构造函数中指定适当的大小,应该没问题。当您调用时,它仍会创建一个副本toByteArray,但这只是暂时的。你真的介意内存短暂上升很多吗?

或者,如果您已经知道开始的大小,您可以创建一个字节数组并重复从 a 读取FileInputStream到该缓冲区,直到您获得所有数据。

于 2011-08-31T09:52:33.570 回答
5

如果您真的想将文件映射到内存中,那么 aFileChannel是适当的机制。

如果您要做的只是将文件读入一个简单的文件byte[](并且不需要对该数组的更改以反映回文件),那么只需byte[]从普通文件中读取一个适当大小的文件FileInputStream就足够了。

Guava可以Files.toByteArray()为您完成所有这些工作。

于 2011-08-31T09:51:48.660 回答
3

有关 的缓冲区增长行为的解释ByteArrayOutputStream,请阅读此答案

在回答您的问题时扩展ByteArrayOutputStream. 在您的情况下,最好覆盖写入方法,以便将最大额外分配限制为 16MB。您不应覆盖toByteArray以公开受保护的 buf[] 成员。这是因为流不是缓冲区。流是具有位置指针和边界保护的缓冲区。因此,从类外部访问和潜在地操作缓冲区是危险的。

于 2012-12-05T13:43:27.223 回答
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 开始,最简单、最快的方法是调用Files.readAllBytes。在 Java 6 及更低版本中,您可以使用DataInputStream.readFully,如Peter Lawrey 的回答。无论哪种方式,您都将获得一个以正确大小分配一次的数组,而无需重复重新分配 ByteArrayOutputStream。

于 2014-11-10T18:54:52.353 回答
2

如果您有 40 MB 的数据,我看不出有任何理由说明为什么创建一个字节 [] 需要超过 40 MB。我假设您正在使用不断增长的 ByteArrayOutputStream,它在完成时会创建一个 byte[] 副本。

您可以尝试一下旧的一次读取文件的方法。

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

如果您可以直接使用 ByteBuffer,则使用 MappedByteBuffer 效率更高,并且可以避免数据副本(或大量使用堆),但是如果您必须使用 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 管理其缓冲区的方式是分配一个初始大小,并且(至少)在缓冲区填满时将缓冲区加倍。因此,在最坏的情况下,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 并重写这个方法,所以直接返回原始数组。考虑到流和字节数组不会被多次使用,这里是否有任何潜在的危险?

潜在的危险是您的假设不正确,或者由于其他人无意中修改了您的代码而变得不正确......

于 2011-08-31T10:15:55.877 回答
2

Google Guava ByteSource似乎是在内存中缓冲的好选择。与ByteArrayOutputStreamor ByteArrayList(来自 Colt 库)之类的实现不同,它不会将数据合并到一个巨大的字节数组中,而是单独存储每个块。一个例子:

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 回答
0

...在读取 1GB 文件时出现了相同的观察结果:Oracle 的 ByteArrayOutputStream 具有惰性内存管理。字节数组由 int 索引,无论如何限制为 2GB。在不依赖 3rd-party 的情况下,您可能会发现这很有用:

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;
    }
}
于 2018-05-27T11:50:51.893 回答