Files.lines的高内存使用率

时间:2018-11-16 18:05:50

标签: java

我在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并将流置于并行模式(消耗更多内存)。

我的问题是: 这是预期的内存使用量吗? 有没有办法使用更少的内存?

3 个答案:

答案 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)

由于以下原因,您计算内存的方式不正确:

  1. 您已使用了总内存(不是已用内存的 )。 JVM延迟分配内存,当分配时,它将按块分配。因此,当需要额外的1字节内存时,它可以分配1MB内存(前提是总内存不超过配置的最大堆大小)。因此,分配的堆内存中有很大一部分可能仍未使用。因此,您需要计算已使用的内存:Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
  2. 使用上述公式可以看到很大一部分内存可能已准备好进行垃圾回收。 JVM在说出OutOfMemory之前肯定会进行垃圾回收。因此,要想出一个主意,您应该在计算使用的内存之前执行System.gc()。当然,您不会在生产中调用gc,而且调用gc并不能保证JVM确实会触发垃圾回收。但是出于测试目的,我认为它很好用。
  3. 在进行流处理时,您获得了OutOfMemory。当时String尚未形成,并且StringBuilder具有很强的参考意义。您应该调用capacity()的{​​{1}}方法来获取StringBuilder中数组中char个元素的实际数量,然后将其乘以2以获取字节数,因为Java内部使用StringBuilder,它需要2个字节来存储ASCII字符。
  4. 最后,代码的编写方式(即,最初没有为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条(例如)记录的数据。