我在SO上发现了一些其他问题,这些问题与我的需求很接近,但我无法弄清楚。我正在逐行读取文本文件,并遇到内存不足错误。这是代码:
System.out.println("Total memory before read: " + Runtime.getRuntime().totalMemory()/1000000 + "MB");
String wp_posts = new String();
try(Stream<String> stream = Files.lines(path, StandardCharsets.UTF_8)){
wp_posts = stream
.filter(line -> line.startsWith("INSERT INTO `wp_posts`"))
.collect(StringBuilder::new, StringBuilder::append,
StringBuilder::append)
.toString();
} catch (Exception e1) {
System.out.println(e1.getMessage());
e1.printStackTrace();
}
try {
System.out.println("wp_posts Mega bytes: " + wp_posts.getBytes("UTF-8").length/1000000);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
System.out.println("Total memory after read: " + Runtime.getRuntime().totalMemory()/1000000 + "MB");
输出就像(在具有更多内存的环境中运行时):
Total memory before read: 255MB
wp_posts Mega bytes: 18
Total memory after read: 1035MB
请注意,在生产环境中,我无法增加内存堆。
我尝试显式关闭流,执行gc并将流置于并行模式(消耗更多内存)。
我的问题是: 这是预期的内存使用量吗? 有没有办法使用更少的内存?
答案 0 :(得分:1)
您的问题出在collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
中。当您向StringBuilder
添加smth并且内部数组不足时,则将其加倍并复制上一个数组的一部分。
执行new StringBuilder(int size)
以预定义内部数组的大小。
第二个问题是您有一个大文件,但结果却将其放入StringBuilder
中。这对我来说很奇怪。实际上,这与在不使用String
的情况下将整个文件读入Stream
相同。
答案 1 :(得分:0)
如果您允许JVM调整堆大小,那么您的Runtime.totalMemory()
计算毫无意义。 Java将根据需要分配堆内存,只要它不超过-Xmx
值即可。由于JVM很聪明,因此它不会一次分配1字节的堆内存,因为这会非常昂贵。取而代之的是,JVM一次将请求更多的内存(实际值取决于平台和JVM实现)。
您的代码当前正在将文件的内容加载到内存中,因此将在堆上创建对象。因此,JVM最有可能从操作系统请求内存,并且您将观察到的Runtime.totalMemory()
值增加了。
尝试使用大小严格的堆来运行程序,例如通过添加-Xms300m -Xmx300m
选项。如果不会得到OutOfMemoryError
,请减少堆,直到得到为止。但是,您还需要注意GC周期,这些事情是相互联系的,而且是折衷方案。
或者,您可以在处理完文件后创建堆转储,然后使用MemoryAnalyzer浏览数据。
答案 2 :(得分:0)
由于以下原因,您计算内存的方式不正确:
Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
System.gc()
。当然,您不会在生产中调用gc,而且调用gc并不能保证JVM确实会触发垃圾回收。但是出于测试目的,我认为它很好用。String
尚未形成,并且StringBuilder
具有很强的参考意义。您应该调用capacity()
的{{1}}方法来获取StringBuilder
中数组中char
个元素的实际数量,然后将其乘以2以获取字节数,因为Java内部使用StringBuilder
,它需要2个字节来存储ASCII字符。UTF16
指定足够大的大小),每次StringBuilder
用完空间时,它都会使内部数组的大小增加一倍创建一个新数组并复制内容。这意味着一次分配的大小将是实际StringBuilder
的三倍。您无法测量此值,因为它发生在String
类中,并且当控件从StringBuilder
类出来时,旧数组已准备好进行垃圾回收。因此,当您收到OutOfMemory错误时,很有可能会在StringBuilder
中尝试分配双倍大小的数组时或更确切地说在StringBuilder
方法中获取错误。让我们考虑一下与您的程序相似的程序。
Arrays.copyOf
在每次附加之后,我正在打印public static void main(String[] arg) {
// Initialize the arraylist to emulate a
// file with 32 lines each containing
// 1000 ASCII characters
List<String> strList = new ArrayList<String>(32);
for (Integer i = 0; i < 32; i++) {
strList.add(String.format("%01000d", i));
}
StringBuilder str = new StringBuilder();
strList.stream().map(element -> {
// Print the number of char
// reserved by the StringBuilder
System.out.print(str.capacity() + ", ");
return element;
}).collect(() -> {
return str;
}, (response, element) -> {
response.append(element);
}, (response, element) -> {
response.append(element);
}).toString();
}
的容量。
程序的输出如下:
StringBuilder
如果文件有“ n”行(n为2的幂),并且每行平均有“ m”个ASCII字符,则在程序执行结束时16, 1000, 2002, 4006, 4006, 8014, 8014, 8014, 8014,
16030, 16030, 16030, 16030, 16030, 16030, 16030, 16030,
32062, 32062, 32062, 32062, 32062, 32062, 32062, 32062,
32062, 32062, 32062, 32062, 32062, 32062, 32062,
的容量为:(n * m + 2 ^(a + 1)),其中(2 ^ a = n)。
例如如果文件有256行,平均每行1500个ASCII字符,则程序末尾StringBuilder
的总容量为:(256 * 1500 + 2 ^ 9)= 384512个字符。
假定文件中只有ASCII字符,每个字符将以UTF-16表示形式占用2个字节。此外,每次StringBuilder
数组空间不足时,都会创建一个更大的新数组,其大小是原始数组的两倍(请参见上面的容量增长数字),并将旧数组的内容复制到新数组中。然后将旧的数组留给垃圾回收。因此,如果您再添加2个^(a + 1)或2 ^ 9个字符,则StringBuilder
将创建一个新数组来保存(n * m + 2 ^(a + 1))* 2 + 2个字符然后开始将旧数组的内容复制到新数组中。因此,随着复制活动的进行,StringBuilder
中将有两个大型数组。
因此,总内存为:384512 * 2 +(384512 * 2 + 2)* 2 = 23,07,076 = 2.2 MB(大约),仅可容纳0.7 MB数据。
我忽略了其他消耗内存的项,例如数组头,对象头,引用等,因为与数组大小相比,这些项可以忽略不计或保持不变。
因此,总而言之,每行1500个字符的256行占用2.2 MB(大约),仅容纳0.7 MB数据(三分之一的数据)。
如果开始时初始化StringBuilder
的大小为3,84,512,那么您可以在三分之一的内存中容纳相同数量的字符,并且所需的工作量也要少得多在数组复制和垃圾回收方面的CPU
最后,在此类问题中,您可能需要分块执行,一旦StringBuilder
的内容处理了1000条记录(例如),就将其写入文件或数据库中, StringBuilder
,然后重新开始下一批记录。因此,您在内存中永远不会保存超过1000条(例如)记录的数据。