2011-08-31 120 views
14

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

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

有人知道更好的方式来做到这一点,而不使用三倍的RAM文件大小?

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

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

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

我想我可能只是延长ByteArrayOutputStream和重写这个方法,因此对原阵列直接返回。鉴于流和字节数组将不会被使用多次,这里是否存在潜在的危险?

+0

同类问题http://stackoverflow.com/questions/964332/java-large-files-disk-io-performance – Santosh

回答

13

MappedByteBuffer可能是你在找什么。

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

+0

感谢您的回答。我试图设定适当的能力,结果是一样的。为此,我更喜欢基于流的东西,因为我应用一些过滤器会很有趣。不过,如果没有其他方法,我会尝试使用这些MappedByteBuffers。 – user683887

5

如果你真的想把这个文件存入内存,那么一个FileChannel是合适的机制。

如果你想要做的就是文件读入到一个简单的byte[](并且不需要更改该数组被反射回文件),然后简单地读成一个大小合适的byte[]从正常FileInputStream应该就够了。

GuavaFiles.toByteArray()这是为你做的一切。

+0

番石榴是这个问题的最佳选择。谢谢。 – danik

10

ByteArrayOutputStream应该没关系,只要你在构造函数中指定一个合适的大小即可。当您拨打toByteArray时,它仍然会创建副本,但这只是临时。你真的介意内存简要往上涨吗?

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

+0

是的,这是暂时的,但我不想使用太多的记忆。我不知道一些文件会有多大,这可能会用在小型机器上,所以我尽量使用尽可能少的内存。 – user683887

+0

@ user683887:那么如何创建我提交的第二个选择?这将只需要尽可能多的数据。如果您需要应用过滤器,则可以始终读取文件两次 - 一次计算出您需要的大小,然后再次实际读取数据。 –

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

使用MappedByteBuffer更有效,避免了数据的拷贝(或使用堆得多)提供您可以直接使用字节缓冲区,但是如果你必须使用一个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文件。

最后一步分配一个确切的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应具有40兆字节的内存使用峰值而读取一个40MB的文件;即没有浪费的空间。


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


我想我可能只是延长ByteArrayOutputStream和重写这个方法,因此对原阵列直接返回。鉴于流和字节数组将不会被使用多次,这里是否存在潜在的危险?

的潜在危险是,你的假设是不正确的,或成为不正确因他人修改你的代码不知不觉...

+0

谢谢@Stephen。你是对的,额外的堆使用是由于BAOS尺寸的初始化不正确,正如我在更新中所描述的。我使用visualvm来测量内存使用情况:不确定它是否是最好的方法。 – user683887

1

有关ByteArrayOutputStream的缓冲液增长行为的说明,请参阅this answer

在回答你的问题时,它可安全延长ByteArrayOutputStream。在你的情况下,重写写入方法可能会更好,因为最大的额外分配是有限的,比如16MB。您不应该覆盖toByteArray以显示受保护的buf []成员。这是因为流不是缓冲区;流是一个具有位置指针和边界保护的缓冲区。所以,从课堂外访问和潜在地操纵缓冲区是很危险的。

1

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(); 
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是使用其writeTo(OutputStream)方法直接传递缓冲所提供的OutputStream事实缓冲区的hackish方式:

/** 
* 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的子类更简单。)

但是,从您的其余问题中,它听起来像是e所有你想要的是文件完整内容的普通byte[]。从Java 7开始,最简单快速的方法是拨打Files.readAllBytes。在Java 6及更低版本中,可以使用DataInputStream.readFully,如Peter Lawrey's answer。无论哪种方式,您将得到一个数组,其分配的一次在正确的大小,没有反复重新分配ByteArrayOutputStream。