Java:内存高效的ByteArrayOutputStream

时间:2011-08-31 09:45:39

标签: java bytearray memory-mapped-files bytearrayoutputstream

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

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

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

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

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

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

我想我可以扩展ByteArrayOutputStream并重写此方法,以便直接返回原始数组。这里有没有潜在的危险,因为流和字节数组不会被多次使用?

9 个答案:

答案 0 :(得分:13)

MappedByteBuffer可能就是你要找的东西。

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

答案 1 :(得分:10)

只要在构造函数中指定适当的大小,

ByteArrayOutputStream就可以了。当您拨打toByteArray时,它仍会创建副本,但这只是临时。你真的介意短暂地记忆了吗?

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

答案 2 :(得分:5)

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

如果您只想将文件读入一个简单的byte[](并且不需要对该数组进行更改以反映回文件),那么只需读入适当大小的byte[]。来自正常FileInputStream的1}}就足够了。

GuavaFiles.toByteArray()为您完成所有这些工作。

答案 3 :(得分:3)

有关ByteArrayOutputStream的缓冲区增长行为的解释,请阅读this answer

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

答案 4 :(得分:2)

如果你有40 MB的数据,我没有看到任何理由为什么创建一个byte []需要超过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 []则不太可能有用。

答案 5 :(得分: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时给出尺寸提示; e.g。

     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 */
    
  4. 选项1和2在读取40Mb文件时应具有40Mb的峰值内存使用量;即没有浪费的空间。


    如果您发布了代码并描述了测量内存使用情况的方法,那将会很有帮助。


      

    我想我可以扩展ByteArrayOutputStream并重写此方法,以便直接返回原始数组。这里有没有潜在的危险,因为流和字节数组不会被多次使用?

    潜在的危险是您的假设不正确,或者由于其他人在不知情的情况下修改您的代码而导致 错误...

答案 6 :(得分:2)

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();

答案 7 :(得分:2)

  

我想我可以扩展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获取缓冲区的另一种但是hackish方法是使用它的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];
}

(这很有效,但我不确定它是否有用,因为Subteing ByteArrayOutputStream更简单。)

然而,从你的问题的其余部分来看,听起来你想要的只是文件完整内容的简单byte[]。从Java 7开始,最简单,最快速的方法是调用Files.readAllBytes。在Java 6及更低版本中,您可以使用DataInputStream.readFully,如Peter Lawrey's answer中所示。无论哪种方式,您将获得一个以正确大小分配一次的数组,而不会重复重新分配ByteArrayOutputStream。

答案 8 :(得分:0)

...在阅读1GB文件时也有同样的观察结果:Oracle的ByteArrayOutputStream具有惰性内存管理。 字节数组由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;
    }
}