2013-07-21 46 views
4

我寫了一段java代碼,在CentOS上創建500K個小文件(每個平均40K)。原來的代碼是這樣的:OutputStream.write(buf,offset,size)在Linux上有內存泄漏嗎?

package MyTest; 

import java.io.*; 

public class SimpleWriter { 

public static void main(String[] args) { 
    String dir = args[0]; 
    int fileCount = Integer.parseInt(args[1]); 

    String content="@#$% SDBSDGSDF ASGSDFFSAGDHFSDSAWE^@$^HNFSGQW%#@&$%^J#%@#^$#UHRGSDSDNDFE$T#@[email protected]%[email protected]^$#@YEGEQW%[email protected]%!!GSDHWET!^"; 
    StringBuilder sb = new StringBuilder(); 
    int count = 40 * 1024/content.length(); 
    int remainder = (40 * 1024) % content.length(); 
    for (int i=0; i < count; i++) 
    { 
     sb.append(content); 
    } 
    if (remainder > 0) 
    { 
     sb.append(content.substring(0, remainder)); 
    } 

    byte[] buf = sb.toString().getBytes(); 

    for (int j=0; j < fileCount; j++) 
    { 
     String path = String.format("%s%sTestFile_%d.txt", dir, File.separator, j); 
     try{ 
      BufferedOutputStream fs = new BufferedOutputStream(new FileOutputStream(path)); 
      fs.write(buf); 
      fs.close(); 
     } 
     catch(FileNotFoundException fe) 
     { 
      System.out.printf("Hit filenot found exception %s", fe.getMessage()); 
     } 
     catch(IOException ie) 
     { 
      System.out.printf("Hit IO exception %s", ie.getMessage()); 

     } 

    } 
} 

    } 

您可以通過以下問題命令來運行這個: Java的罐子SimpleWriter.jar my_test_dir 500000

我認爲這是一個簡單的代碼,但後來我意識到,這代碼使用高達14G的內存。我知道,因爲當我使用-m來檢查內存時,可用內存一直在下降,直到我的15G內存虛擬機只剩下70 MB可用內存。我使用Eclipse進行編譯,然後根據JDK 1.6和JDK1.7進行編譯。結果是一樣的。有趣的是,如果我註釋掉fs.write(),只需打開並關閉流,內存就會穩定在某個點。一旦我把fs.write()放回去,內存分配就會變得瘋狂。 500K 40KB文件大約20G。看起來Java的流編寫器從不在操作過程中釋放它的緩衝區。

我曾經認爲java GC沒有時間清理。但是這是沒有意義的,因爲我關閉了每個文件的文件流。我甚至將我的代碼轉移到C#中,並在windows下運行,生成的500K 40KB文件的相同代碼在某些時刻保持穩定,而不是在CentOS下使用14G。至少C#的行爲是我所期望的,但我不相信Java以這種方式執行。我問我在java中有經驗的同事。他們在代碼中看不到任何錯誤,但無法解釋爲什麼會發生這種情況。他們承認沒有人曾經試圖在一個循環中不停地創建500K文件。

我也在網上搜索,大家都說唯一需要注意的就是關閉流,我做了。

任何人都可以幫我弄清楚有什麼問題嗎?

任何人都可以試試這個,告訴我你看到了什麼嗎?

順便說一句,在這個社區的一些人試圖在Windows上的代碼,它似乎工作正常。我沒有在Windows上試過。我只在Linux中嘗試,因爲我認爲人們使用Java的地方。所以,似乎這個問題發生在Linux上)。

我也做了以下限制JVM堆,卻拿不出效果 的java -Xmx2048m -jar SimpleWriter.jar my_test_dir 500000

+0

「代碼是這樣的」 - 你的意思是你寫了一些類似於你的代碼併發布的代碼,或者這是你實際代碼中的代碼片段嗎?如果這不是你的實際代碼,是否運行這會導致14 GB的內存消耗? – user2357112

+0

奇怪。你以後不會碰到這些文件,是嗎? – user2357112

+0

這是一個內存中的文件系統嗎? – user2357112

回答

-1

我認爲這是與您正在使用BufferedOutputStream的事實。所有的緩衝可以很容易地使用15 GB的內存。

+1

不太可能。當輸出流是GC'd時,緩衝區應該被丟棄,反正它不是那麼大。 – user2357112

+0

我最初直接使用FileOutputSteam,並且遇到了我看到的內存問題。然後,我改變了使用BufferedOutputStream,這似乎很多網上文章推薦,我得到了相同的結果(內存明智)。 – tttzhang2000

+0

默認情況下,BufferedOutputStream使用8k字節的緩衝區。要佔用14GB內存,必須同時存在14 * 1024 * 1024 * 1024/8/1024個實例。答案不計算。 -1 – EJP

0

[EDIT2:原來的答案留在斜體在這篇文章的末尾]

在評論你的澄清之後,我已經運行在Windows機器上的代碼(Java 1.6)和這裏是我的調查結果(帶有編號的VisualVM,從任務管理器中看到操作系統內存):

  • 例如用40K大小,寫入500K的文件(無參數JVM): 使用的堆:〜4M,總堆:16M,操作系統內存:〜16M

  • 具有40M大小的示例,寫入500個文件(參數爲JVM -Xms128m -Xmx512m。不帶參數創建的StringBuilder)時,我得到一個內存不足錯誤: 使用的堆:〜265M,堆大小:〜365M,操作系統內存:〜365M

特別是從第二個例子中,你可以看到,我原來的解釋仍站立。是的,有人會認爲大多數內存將被釋放,因爲BufferedOutputStreambyte[]駐留在第一代空間(短命對象)中,但是a)不會立即發生,並且b)當GC決定啓動時在我的情況下),是的,它會嘗試清除內存,但它可以清除儘可能多的內存,而不一定是所有內存。 GC不提供任何您可以信賴的擔保人。

所以一般來說,你應該給JVM儘可能多的內存,讓你感覺舒服。如果您需要爲特殊功能保持低內存,您應該嘗試一個策略,作爲我在原始答案中給出的代碼示例,即不要創建所有這些對象。

現在對於CentOS來說,看起來JVM的行爲很奇怪。也許我們可以談論一個錯誤或不好的實現。要將它歸類爲漏洞/錯誤,儘管您應該嘗試使用-Xmx來限制堆。另外請嘗試使用Peter Lawrey suggested來根本不創建BufferedOutputStream(在小文件的情況下),因爲您只需一次寫入所有字節。

如果它仍然超出內存限制,那麼你遇到了泄漏,應該可能會提交一個錯誤。 (儘管如此,你仍然可以抱怨,他們可能會在未來優化它)。


[EDIT1:下面的答案假設OP的代碼進行儘可能多的讀操作的寫操作,所以內存的使用是合理的。該OP澄清這是不是這樣的,所以他的問題不回答

「......我的記憶15G VM ......」 如果你給JVM的內存爲什麼要嘗試運行GC?就JVM而言,它允許從系統獲得儘可能多的內存,並且只有在認爲適當時才運行GC。 BufferedOutputStream的每次執行都會默認分配一個8K大小的緩衝區。 JVM只會在需要時嘗試回收該內存。這是預期的行爲。 不要混淆你認爲從系統角度和JVM角度來看的內存。就係統而言,分配內存將在JVM關閉時釋放。就JVM而言,從BufferedOutputStream分配的所有byte[]陣列都不再使用,它​​是「空閒」內存,如果需要的話,它將被回收。 如果由於某種原因您不想要這種行爲,您可以嘗試以下操作:擴展BufferedOutputStream類(例如創建一個ReusableBufferedOutputStream類)並添加一個新方法,例如reUseWithStream(OutputStream os)。然後該方法將清除內部byte[],刷新並關閉前一個流,重置使用的任何變量並設置新的流。然後,您的代碼將成爲如下:

// intialize once 
ReusableBufferedOutputStream fs = new ReusableBufferedOutputStream(); 
for (int i=0; i < fileCount; i ++) 
{ 
    String path = String.format("%s%sTestFile_%d.txt", dir, File.separator, i); 

    //set the new stream to be buffered and read 
    fs.reUseWithStream(new FileOutputStream(path)); 
    fs.write(this._buf, 0, this._buf.length); // this._buf was allocated once, 40K long contain text 
} 
fs.close(); // Close the stream after we are done 

使用上面的方法,你會避免創建多個byte[]。但是,我沒有看到預期行爲的任何問題,除了「我看到它佔用太多內存」之外,沒有提到任何問題。畢竟,你已經準備好使用它了。]

+0

我認爲大多數JVM的收集頻率比這更頻繁,即使它們具有可用內存。不過,這是一個很明智的想法。顯式調用'Runtime.getRuntime().gc()'每隔100次迭代將會是一件好事。 – user2357112

+0

@ user2357112不是真的。在一般情況下,當Java處於負載狀態(正在運行)時,Java會像嘗試索取儘可能多的內存。如果所有線程都無所事事,最終它將回收內存。調用'Runtime.getRuntime().gc()'只不過是一個請求。 JVM可以忽略它(通常它會) –

+0

我不相信像java這樣的工業強度語言會像這裏描述的那樣行事。系統如何收集垃圾並等到需要時再進行收集。當發現內存需要回收時,很可能已經太晚了,因爲無人申請的內存會佔用過多的資源,甚至操作系統也無法執行。在開始之後,我在這之後添加了讀取操作,並且因爲寫入操作吃掉了所有內存,所以我的讀取速度比寫入速度慢得多,這對於今天的文件系統來說是不正常的。如果Java以這種方式設計,這將是一個很大的缺陷。 – tttzhang2000

1

我試圖在Win XP,JDK 1.7.25上測試你的編程。立即得到OutOfMemoryExceptions。

調試時,只有3000計數(參數[1]),計數從該代碼變量:

int count = 40 * 1024 * 1024/content.length(); 
    int remainder = (40 * 1024 * 1024) % content.length(); 
    for (int i = 0; i < count; i++) { 
     sb.append(content); 
    } 

數爲355449.所以,你要創建的字符串將是355449項*內容長,或者如你所計算的那樣,長度爲40Mb。當我266587歲時,我的記憶已經不復存在,並且有31457266字符長。在哪一點我得到的每個文件是30Mb。

這個問題似乎沒有與內存或GC,但與你打包字符串的方式。

您是否在創建任何文件之前看到創建的文件或內存已耗盡?

我覺得你的問題主要是線:

int count = 40 * 1024 * 1024/content.length(); 

應該是:

int count = 40 * 1024/content.length(); 

創建40K,而不是40兆字節的文件。

+0

我再次發佈了我的程序。它不會使用BufferedOutputStream,它只創建40K文件緩衝區一次。它在Linux中仍然造成同樣的問題。你能幫我解決Linux下的問題嗎? – tttzhang2000

+0

我現在無法訪問Linux機器。但是當我分析應用程序時,我沒有看到任何內存泄漏。您可以嘗試使用NetBeans Profiler? – Ayman

+0

@ tttzhang2000在Linux下使用兩個不同的jvms更新後的版本對我來說運行得很好。 – kiheru