2011-08-31 137 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。