0

我有两个大型 CSV 文件,其中包含 Web 应用程序用户验证某些信息所需的数据。我定义了一个 ArrayList< String[] > 并打算将这两个文件的内容保存在内存中,这样我就不必每次用户登录并使用应用程序时都读取它们。

但是,在初始化应用程序并尝试读取第二个文件时,我得到了 java.lang.OutOfMemoryError: Java heap space。(它很好地完成了第一个文件的读取,但是在读取第二个文件时挂起,过了一会儿我得到了那个异常)

读取文件的代码非常简单:

ArrayList<String[]> tokenizedLines = new ArrayList<String[]>();

public void parseTokensFile() throws Exception {
    BufferedReader bRead = null;
    FileReader fRead = null;

    try {
        fRead = new FileReader(this.tokensFile);
        bRead = new BufferedReader(fRead);
        String line;
        while ((line = bRead.readLine()) != null) {
            tokenizedLines.add(StringUtils.split(line, fieldSeparator));
        }
    } catch (Exception e) {
        throw new Exception("Error parsing file.");
    } finally {
        bRead.close();
        fRead.close();
    }
}

我读过Java的split函数在读取大量数据时可能会占用大量内存,因为子字符串函数引用了原始字符串,因此某些字符串的子字符串将占用与原始字符串相同的内存量,即使我们只需要几个字符,所以我做了一个简单的拆分函数来避免这种情况:

public String[] split(String inputString, String separator) {
    ArrayList<String> storage = new ArrayList<String>();
    String remainder = new String(inputString);
    int separatorLength = separator.length();
    while (remainder.length() > 0) {
        int nextOccurance = remainder.indexOf(separator);
        if (nextOccurance != -1) {
            storage.add(new String(remainder.substring(0, nextOccurance)));
            remainder = new String(remainder.substring(nextOccurance +  separatorLength));
        } else {
            break;
        }
    }

    storage.add(remainder);
    String[] tokenizedFields = storage.toArray(new String[storage.size()]);
    storage = null;

    return tokenizedFields;

}

不过,这给了我同样的错误,所以我想知道这是否不是内存泄漏,而只是我不能在内存中有这么多对象的结构。一个文件长约 600,000 行,每行 5 个字段,另一个文件长约 900,000 行,每行字段数量大致相同。

完整的堆栈跟踪是:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at xxx.xxx.xxx.StringUtils.split(StringUtils.java:16)
    at xxx.xxx.xxx.GFTokensFile.parseTokensFile(GFTokensFile.java:36)

那么,在长篇文章之后(对不起:P),这是对分配给我的 JVM 的内存量的限制,还是我遗漏了一些明显的东西并在某处浪费了资源?

4

5 回答 5

4

在具有 4GB RAM 的 32 位操作系统上,您的 JVM 不会超过 2GB。这是一个上限。

第二个是您在启动 JVM 时指定的最大堆大小。看看那个 -Xmx 参数。

第三个事实是,您无法将 X 单位的任何东西放入 X > Y 的 Y 大小的容器中。您知道文件的大小。尝试单独解析每个并查看它们正在消耗什么样的堆。

我建议你下载Visual VM,安装所有可用的插件,并让它在你的应用程序运行时监控它。您将能够看到整个堆、perm gen 空间、GC 收集、哪些对象占用了最多的内存等。

获取数据对于所有问题都是无价的,尤其是像这样的问题。没有它,你只是在猜测。

于 2012-06-02T14:48:09.123 回答
2

我在程序的原始版本中看不到存储泄漏。

类似方法可能泄漏大量存储的情况split相当有限:

  1. 您不必保留对您拆分的原始字符串的引用。

  2. 您需要保留对字符串拆分生成的字符串子集的引用。

调用时发生的情况String.substring()是它创建了一个新的 String 对象,该对象共享原始 String 的支持数组。如果原始 String 引用随后被垃圾回收,则子字符串 String 现在持有一个字符数组,其中包括不在子字符串“中”的字符。这可能是存储泄漏,具体取决于子字符串的保留时间。

在您的示例中,您将包含所有字符的字符串分开作为字段分隔符。很有可能这实际上节省了空间......与每个子字符串是独立字符串时使用的空间相比。当然,您的版本split不能解决问题也就不足为奇了。

我认为您需要增加堆大小,或者更改您的应用程序,以便它不需要同时将所有数据保存在内存中。

于 2012-06-02T14:59:19.543 回答
1

尝试改进您的代码或将数据处理留给数据库。

  1. 内存使用量随文件大小而变大,因为代码会生成已处理数据的冗余副本。有一个要处理的一个已处理的和一些部分数据。String 是不可变的,请参见此处,无需使用new String(...)来存储结果, split 已经完成了该副本。

  2. 如果可以,将整个数据存储和搜索委托给数据库。CSV 文件很容易导入/导出到数据库,并且它们完成了所有艰苦的工作。

于 2012-06-02T15:11:36.050 回答
0

确保两个文件的总长度小于您的堆大小。您可以使用 JVM 选项设置最大堆大小-Xmx

那么如果你有这么多的内容,也许你不应该把它完全加载到内存中。有一次我遇到了类似的问题,我使用一个将信息索引存储在大文件中的索引文件来修复它。然后我只需要在良好的偏移量处读取一行。

在你的 split 方法中也有一些奇怪的事情。

String remainder = new String(inputString);

您不必inputString使用副本来保存,String 是不可变的,因此更改仅适用于 split 方法的范围。

于 2012-06-02T15:14:37.317 回答
0

虽然我不建议您对正在做的事情进行实际的字符串实习,但使用该技术背后的想法怎么样?您可以使用 HashSet 或 HashMap 来确保您只使用单个 String 实例,只要您的数据包含相同的字符序列。我的意思是,数据中一定有某种重叠,对吧?

另一方面,您在这里看到的可能是堆碎片的坏情况。我不确定 JVM 是如何处理这些情况的,但是在 Microsoft CLR 中,较大的对象(尤其是数组)将被分配在一个单独的堆上。增长策略,例如 ArrayList 的策略将创建一个更大的数组,然后在释放对它的引用之前复制前一个数组的内容。大对象堆 (LOH) 未在 CLR 中压缩,因此这种增长策略将留下大量可用内存区域,ArrayList 无法再使用。

我不知道其中有多少适用于 Lava VM,但您可以尝试先使用 LinkedList 构建列表,然后将列表内容转储到 ArrayList 或直接转储到数组中。这样,大型行数组将只创建一次,而不会造成任何碎片。

于 2012-06-02T15:20:23.450 回答