OutputStream.write(buf,offset,size)在Linux上是否有内存泄漏?

时间:2013-07-21 07:13:38

标签: java memory-leaks outputstream

我在CentOS上写了一段java代码来创建500K小文件(平均每个40K)。原始代码是这样的:

 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#@$UERDFASGWQR!@%!@^$#@YEGEQW%!@%!!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 -jar SimpleWriter.jar my_test_dir 500000

我认为这是一个简单的代码,但后来我意识到这段代码使用了高达14G的内存。我知道因为当我使用free -m检查内存时,可用内存不断下降,直到我的15G内存VM只剩下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

3 个答案:

答案 0 :(得分:1)

我试图在Win XP,JDK 1.7.25上测试你的编程。立即获得OutOfMemoryExceptions。

在调试时,只有3000个计数(args [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);
    }

count是355449.因此,您尝试创建的String将是355449 *内容长,或者在您计算时,长度为40Mb。当我266587时,我失去了记忆,而某人长了31457266。我得到的每个文件都是30Mb。

问题似乎不是内存或GC,而是你打包字符串的方式。

在创建任何文件之前,您是否看到创建的文件或内存耗尽?

我认为你的主要问题是这一行:

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

应该是:

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

创建40K,而不是40Mb文件。

答案 1 :(得分:0)

[ Edit2:原帖在本帖末尾留在斜体中]

在评论中做出澄清之后,我已经在Windows机器上运行了您的代码(Java 1.6),这是我的发现(数字来自VisualVM,从任务管理器看到的操作系统内存):

  • 40K大小的示例,写入500K文件(没有参数到JVM): 使用堆:~4M,总堆数:16M,操作系统内存:~16M

  • 40M大小的示例,写入500个文件(参数为JVM -Xms128m -Xmx512m。没有参数,我在创建StringBuilder时出现OutOfMemory错误): 使用堆:~265M,堆大小:~365M,操作系统内存:~365M

特别是从第二个例子中你可以看到我原来的解释仍然存在。是的,有人会期望大部分内存将被释放,因为byte[]的{​​{1}}驻留在第一代空间(短期对象)中,但是a)不会立即发生,并且b)当GC时决定踢(它实际上是在我的情况下),是的,它会尝试清除记忆,但它可以清除尽可能多的内存,不一定全部。 GC不提供您可以依赖的任何保证人。

一般来说,你应该给JVM尽可能多的内存。如果你需要为特殊功能保持低内存,你应该尝试一个策略作为我在原始答案中给出的代码示例,即只是不要创建所有这些BufferedOutputStream对象。

现在在你的CentOS案例中,似乎JVM的行为很奇怪。也许我们可以谈论一个错误或糟糕的实施。要将其归类为泄漏/错误,您应该尝试使用byte[]来限制堆。另外,请尝试Peter Lawrey suggested以完全不创建-Xmx(在小文件中),因为您只需一次写入所有字节。

如果它仍然超出内存限制,那么您遇到了泄漏,应该提交错误。 (你仍然可以抱怨,他们将来可能会对其进行优化)。


[编辑1:下面的答案假设OP的代码执行了与写入操作一样多的读取操作,因此内存使用是合理的。 OP澄清了情况并非如此,所以他的问题没有得到解答

“......我的15G内存VM ...” 如果你为JVM提供尽可能多的内存,为什么它应该尝试运行GC?就JVM而言,允许从系统获取尽可能多的内存,并且只有在认为适合这样做时才运行GC。 BufferedOutputStream的每次执行都会默认分配8K大小的缓冲区。 JVM将尝试仅在需要时回收该内存。这是预期的行为。 不要将您看到的内存从系统的角度和JVM的角度混淆。就系统而言,内存已分配,并将在JVM关闭时释放。就JVM而言,从BufferedOutputStream分配的所有byte[]数组都不再使用,它​​是“免费”内存,如果需要,将被回收。 如果由于某种原因您不希望出现这种情况,可以尝试以下操作:扩展BufferedOutputStream类(例如创建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 。但是我没有看到预期行为有任何问题,除了“我看到它需要太多内存”之外,你没有提到任何问题。毕竟你已经将它用于使用它。]

答案 2 :(得分:-1)

我认为这与您使用BufferedOutputStream的事实有关。所有缓冲都可以轻松使用15 GB内存。