Java:内存高效的ByteArrayOutputStream

20

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

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

有人知道更好的方法吗,而不使用三倍于RAM的文件大小?

更新:谢谢你们的答案。我注意到我可以通过将ByteArrayOutputStream的初始大小设置为略大于原始文件大小来减少内存消耗(使用我的代码的确切大小会强制重新分配,必须检查原因)。

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

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

我在考虑是否可以扩展ByteArrayOutputStream并重写这个方法,从而直接返回原始数组。假设流和字节数组不会再被使用,这种做法存在潜在的危险吗?


类似的问题请查看 https://dev59.com/uHNA5IYBdhLWcg3wbNQ4 - Santosh
9个回答

14

MappedByteBuffer可能是您正在寻找的内容。

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


感谢您的回答。我尝试设置了适当的容量,但结果相同。因此,我更喜欢基于流的方法,这样我可以应用一些过滤器。不过,如果没有其他办法,我会尝试使用那些MappedByteBuffers。 - user683887

10

ByteArrayOutputStream 应该可以胜任,只要在构造函数中指定适当的大小即可。当您调用 toByteArray 时,它仍然会创建一个副本,但那只是临时的。你真的介意内存短暂地增加很多吗?

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


是的,这是临时的,但我更喜欢不使用太多内存。我不知道一些文件会有多大,而且这可能会在小型机器上使用,所以我尽量使用尽可能少的内存。 - user683887
@user683887:那么创建我提出的第二个选择怎么样?那只需要所需的数据量。如果需要应用过滤器,您可以读取文件两次-一次用于计算所需大小,然后再次读取数据。 - Jon Skeet

5
如果您真的想将文件映射到内存中,那么FileChannel就是适当的机制。
如果您只想将文件读入简单的byte[](并且不需要将该数组的更改反映回文件),那么从普通的FileInputStream中读取一个大小适当的byte[]应该就足够了。 Guava提供了Files.toByteArray()函数,可以为您完成这一切。

Guava是解决这个问题的最佳选择。谢谢。 - danik

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。

3

如果您想了解ByteArrayOutputStream缓冲区增长行为的解释,请阅读此答案

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


2
如果你有40 MB的数据,我认为创建一个byte[]不应该超过40 MB。我假设你正在使用一个Growing ByteArrayOutputStream来创建一个byte[]拷贝。
你可以尝试一次性读取文件的旧方法。
File file = 
DataInputStream is = new DataInputStream(FileInputStream(file));
byte[] bytes = new byte[(int) file.length()];
is.readFully(bytes);
is.close();

使用MappedByteBuffer更高效,并避免了数据的复制(或过多使用堆内存),前提是你可以直接使用ByteBuffer。但是,如果你必须使用byte[],那么这种方法就不太会有帮助。

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 的文件。
最后一步是分配一个新数组,用于精确地容纳缓冲区的内容,即 40Mb。因此,实际上正在使用的峰值内存量应为 120Mb。
那么这额外的 40Mb 是在哪里使用的呢?我的猜测是它们并没有被使用,而您实际上正在报告总堆大小,而不是被可达对象占用的内存量。
那么解决方案是什么?
  1. You could use a memory mapped buffer.

  2. You could give a size hint when you allocate the ByteArrayOutputStream; e.g.

     ByteArrayOutputStream baos = ByteArrayOutputStream(file.size());
    
  3. You could dispense with the ByteArrayOutputStream entirely and read directly into a byte array.

     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并重新编写这个方法,以便直接返回原始数组。这里是否存在潜在的危险,因为流和字节数组不会被多次使用?」

谢谢,@Stephen。你是对的,额外的堆使用是由于BAOS大小的错误初始化引起的,正如我在更新中描述的那样。我正在使用visualvm来测量内存使用情况:不确定它是否是最佳方法。 - user683887

2

Google Guava ByteSource 看起来是在内存中进行缓冲的一个不错选择。与 ByteArrayOutputStream 或者 ByteArrayList(来自 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);
< p > ByteSource 可以随时作为 InputStream 读取:

InputStream data = body.openBufferedStream();

0

...当阅读1GB文件时,我也注意到了同样的情况:Oracle的ByteArrayOutputStream具有惰性内存管理。一个byte数组由一个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;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接