Java:内存高效的ByteArrayOutputStream

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

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

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

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

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

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

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

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

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

只要在构造函数中指定了合适的大小, ByteArrayOutputStream就可以了。 当你调用toByteArray时它仍会创建一个副本,但这只是暂时的 。 你真的介意记忆短暂上升吗?

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

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

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

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

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

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

如果你有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(); 

使用MappedByteBuffer更有效并且避免使用数据副本(或使用堆很多),前提是您可以直接使用ByteBuffer,但是如果必须使用byte []则不太可能有用。

…但我发现在复制操作期间的某个时刻需要大约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并重写此方法,以便直接返回原始数组。 这里有没有潜在的危险,因为流和字节数组不会被多次使用?

潜在的危险是您的假设不正确,或由于其他人在不知情的情况下修改您的代码而变得不正确……

我想我可以扩展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]; } 

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

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

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

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

…在阅读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; } }