在Java中加载大型文本文件的最佳方法

5

我有一个文本文件,每行都有一串整数:

47202 1457 51821 59788 
49330 98706 36031 16399 1465
...

该文件有300万行数据,并且需要将其加载到内存中,提取其中的5元组,然后进行一些统计。但是我只拥有8GB RAM的内存限制。为了尽可能减少创建的对象数量,我只使用1个类,其中包含6个浮点变量和一些方法。每行数据会生成该类的若干个对象(与该行数据的单词数成比例)。我开始觉得当C++存在时,Java不适合处理这些事情。
编辑:假设每行数据会产生(n-1)个该类的对象,其中n是由空格分隔的该行数据中的标记数(即1457)。因此,考虑每行平均大小为10个单词,每行平均映射到9个对象上。因此,将会生成9*3*10^6个对象。所需内存量为:9*3*10^6 *(8字节对象头+6 x 4字节浮点数)+(一个map(String,Objects)和另一个map(Integer,ArrayList(Objects)))。我需要将所有内容都保存在内存中,因为后面会进行一些数学优化操作。

你的问题是什么,确切地说? - Dawood ibn Kareem
这里的技巧是逐行阅读,而不是将整个文件读入一个字符串中。 - Davio
1
3百万行?那会占用多少MB呢?100MB?这并不算太大。顺便说一下,“只有1个类”并不意味着你实际创建的对象数量。 - qqilihq
1
(10个单词x 5个字符+9个空格+1行末)x 2字节x 300万行=约630 MB的原始文本。10个对象x(8个标题+24个字段)x 300万=约915 MB的对象。要获得更节省空间的地图,请查看trove-http://trove.starlight-systems.com/。将文件解析为对象后,您不需要它。即使使用非常天真的方法,您仍应该适合2GB以下。 - radai
2
任何你无法访问(即没有指针指向)的东西都将被垃圾回收。因此,一个简单的readLine()循环,从BufferedReader读取一行字符串并输出约10个对象,将产生大量短暂的字符串,然后将被GC回收。你不需要显式地销毁任何东西,只需不保留对你不再需要的东西的引用即可。 - radai
显示剩余2条评论
2个回答

14

读取/解析文件:

处理大型文件的最佳方法是尽量避免将其加载到内存中,这适用于任何语言。

在Java中,可以查看MappedByteBuffer。它允许您将文件映射到进程内存中,并在不将整个文件加载到堆中的情况下访问其内容。

还可以尝试逐行读取文件,并在读取每行后丢弃该行-同样是为了避免一次性将整个文件保存在内存中。

处理生成的对象

对于处理解析时生成的对象,有几种选择:

  1. 和文件本身一样,如果您可以在“流式传输”文件的同时执行想要执行的任何操作而无需将所有内容保存在内存中,则这是最佳解决方案。由于您没有描述您试图解决的问题,因此我不知道是否可能实现。

  2. 某种形式的压缩 - 切换从包装器对象(Float)到基元(float),使用类似享元模式的东西将数据存储在巨大的float[]数组中,并仅构造短暂的对象以访问它,在您的数据中找到一些模式,使您可以更紧凑地存储它

  3. 缓存/卸载 - 如果您的数据仍然无法适应内存,请将其“分页”到磁盘上。这可以简单地扩展guava以分页到磁盘或引入像ehcache或类似的库。

关于Java集合和特别是映射的注释

对于小对象,尤其是Java集合和映射,会产生很大的内存开销(主要是由于所有内容都被包装为对象以及Map.Entry内部类实例的存在)。如果内存消耗是一个问题,您应该考虑使用gnu trove集合,尽管API略微不够优雅。

虽然这是真的,但在这种情况下可能并不是必要的。该文件有300万行,两个示例行中较长的一行为28个字符。假设每行平均约30个字符,并且由于Java的char占用两个字节,因此仅需大约180MB的RAM。 - Wyzard
2
他其实从来没有说过它不适合。我想这可能只是一种过早优化的情况。 - Wyzard
原始文件大小为180MB,每个对象包含8字节的对象头和6个4字节的浮点数,共计300万个对象。所有对象所需的空间为3百万 x (8字节的对象头 + 6个4字节的浮点数)。此外,为了容纳所有对象,还需要12字节的数组头和3百万 x 8字节的指针数组。因此,整个程序所需的空间为180MB + 114MB = ~300MB。我也猜不准。 - radai
Wyzard是正确的。它适合内存。问题更多地在于优化方面。 - user3639557
@user3639557 - 添加了一些关于最小化集合内存成本的信息。 - radai
显示剩余3条评论

0

最好只保留整数和行尾符。

为此,一种方法是将文件转换为两个文件:

  • 一个整数二进制文件(4字节)
  • 一个二进制文件,其中包含下一行开始的索引。

可以使用Scanner进行读取,使用DataOutputStream+BufferedOutputStream进行写入。

然后,您可以将这两个文件加载到原始类型的数组中:

int[] integers = new int[(int)integersFile.length() / 4];
int[] lineEnds = new int[(int)lineEndsFile.length() / 4];

可以使用MappedByteBuffer.toIntBuffer()进行读取。(那么,您甚至不需要数组,但它会变得有点COBOL般的冗长。)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接