我有两个大型CSV文件,其中包含Web应用程序用户验证某些信息所需的数据。我定义了一个ArrayList< String []>并且打算将这两个文件的内容保存在内存中,这样每次用户登录并使用该应用程序时都不必阅读它们。
我在获取java.lang.OutOfMemoryError:Java堆空间时,初始化应用程序并尝试读取第二个文件。 (它完成了读取第一个文件就好了,但在读取第二个文件时挂起,过了一会儿我得到了那个例外)
阅读文件的代码非常简单:
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函数在读取大量数据时会占用大量内存,因为substring函数会引用原始字符串,因此某些String的子字符串将占用与原始字符串相同的内存量,即使我们只想要几个字符,所以我做了一个简单的拆分功能来试图避免这个:
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分配的内存量的限制,还是我错过了一些明显的东西并在某处浪费资源?
答案 0 :(得分:4)
在具有4GB RAM的32位操作系统上,您的JVM不会超过2GB。这是一个上限。
第二个是启动JVM时指定的最大堆大小。看看-Xmx参数。
第三个是生活中的事实,你不能
我建议你下载Visual VM,安装所有可用的插件,让它在应用程序运行时监控它。您将能够看到整个堆,perm gen空间,GC集合,哪些对象占用的内存最多等等。
获取数据对于所有问题都是非常宝贵的,尤其是像这样的问题。没有它,你只是在猜测。
答案 1 :(得分:2)
我无法在程序的原始版本中看到存储泄漏。
split
和类似方法可能泄漏大量存储的情况受到限制:
您必须不保留对您拆分的原始字符串的引用。
您需要保留对字符串拆分生成的字符串的子集的引用。
调用String.substring()
时会发生什么,它会创建一个新的String对象,共享原始String的后备阵列。如果原始的String引用随后被垃圾收集,那么子字符串String现在保持在包含不在子字符串“中”的字符的字符数组中。这可能是存储泄漏,具体取决于保留子字符串的时间长度。
在您的示例中,您将保留包含字段分隔符的所有字符的字符串。很有可能这实际上是保存空间...与每个子字符串是独立字符串时使用的空间相比。当然,您的split
版本无法解决问题也就不足为奇了。
我认为您需要增加堆大小,或者更改应用程序,以便它不需要同时将所有数据保存在内存中。
答案 2 :(得分:1)
尝试改进代码或将数据处理留给数据库。
由于代码会生成已处理数据的冗余副本,因此文件大小会占用内存。有一个要处理一个已处理和一些部分数据。 字符串是不可变的,请参阅here,无需使用 new String(...)来存储结果,split已经复制了该文件。
如果可以,请将整个数据存储委派并搜索到数据库。 CSV文件很容易导入/导出到数据库,他们可以完成所有艰苦的工作。
答案 3 :(得分:0)
确保两个文件的总长度低于堆大小。您可以使用JVM选项-Xmx
设置最大堆大小。
然后,如果你有这么多内容,也许你不应该把它完全加载到内存中。有一次我有类似的问题,我使用索引文件修复它,该文件存储大文件中的信息索引。然后我只需要在良好的偏移处读取一行。
同样在你的分裂方法中有一些奇怪的东西。
String remainder = new String(inputString);
您不必使用副本来保留inputString
,字符串是不可变的,因此更改仅适用于split方法的范围。
答案 4 :(得分:0)
虽然我不建议您正在进行实际的字符串实习,但如何使用该技术背后的想法呢?您可以使用HashSet或HashMap确保只要数据包含相同的字符序列,就只使用单个String实例。我的意思是,数据中必定存在某种重叠,对吗?
另一方面,您可能在这里看到的可能是堆碎片的坏情况。我不确定JVM如何处理这些情况,但在Microsoft CLR中,较大的对象(尤其是数组)将在单独的堆上分配。增长策略(例如ArrayList的增长策略)将创建一个更大的数组,然后在释放对它的引用之前复制先前数组的内容。 CLR中没有压缩大对象堆(LOH),因此这种增长策略将留下ArrayList无法再使用的大量可用内存区域。
我不知道有多少适用于Lava VM,但您可以尝试首先使用LinkedList构建列表,然后将列表内容转储到ArrayList或直接转储到数组中。这样,大量的行只会创建一次,而不会造成任何碎片。