4

我编写了一段 java 代码在 CentOS 上创建 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

4

2 回答 2

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

计数为 355449。因此,您尝试创建的字符串长度为 355449 * 内容,或者根据您的计算,长度为 40Mb。我在 266587 时内存不足,而 sb 的长度为 31457266 个字符。此时我得到的每个文件都是 30Mb。

问题似乎不在于内存或 GC,而在于您创建字符串的方式。

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

我认为你的主要问题是这条线:

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

应该:

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

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

于 2013-07-21T10:10:50.417 回答
0

[ Edit2:原始答案在本文末尾以斜体字显示]

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

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

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

特别是从第二个示例中,您可以看到我最初的解释仍然有效。是的,有人会期望大部分内存将被释放,因为byte[]驻留BufferedOutputStream在第一代空间(短期对象)中,但这 a) 不会立即发生,并且 b) 当 GC 决定启动时(它实际上在我的case),是的,它会尝试清除内存,但它可以清除它认为合适的内存,不一定是全部。GC 不提供任何您可以信赖的保证。

所以一般来说,你应该给 JVM 尽可能多的你觉得舒服的内存。如果您需要为特殊功能保持低内存,您应该尝试一种策略,就像我在原始答案中给出的代码示例一样,即不要创建所有这些byte[]对象。

现在,在您使用 CentOS 的情况下,JVM 的行为似乎确实很奇怪。也许我们可以谈论一个错误或糟糕的实现。要将其归类为泄漏/错误,尽管您应该尝试使用-Xmx来限制堆。另外请尝试Peter Lawrey 建议 不要创建的方法BufferedOutputStream(在小文件的情况下),因为您只需一次写入所有字节。

如果它仍然超过内存限制,那么您遇到了泄漏并且可能应该提交一个错误。(尽管您仍然可以抱怨,他们可能会在未来对其进行优化)。


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

“...我的 15G 内存 VM...” 如果您给 JVM 尽可能多的内存,为什么它应该尝试运行 GC?就 JVM 而言,它被允许从系统中获取尽可能多的内存并仅在它认为合适的时候运行 GC。默认情况下,每次执行BufferedOutputStream都会分配一个 8K 大小的缓冲区。JVM 只会在需要时尝试回收该内存。这是预期的行为。不要混淆从系统的角度和 JVM 的角度来看您认为是空闲的内存。就系统而言,内存已分配并将在 JVM 关闭时释放。就 JVM 而言,所有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

使用上述方法,您将避免创建许多byte[]. 但是,我认为预期的行为没有任何问题,除了“我看到它需要太多内存”之外,您也没有提到任何问题。毕竟你已经配置它来使用它。]

于 2013-07-21T08:14:03.570 回答