如何在Java中处理大数据集而不使用过多的内存

5
我正在使用Java进行开发。我的要求是必须比较两个数据库查询。为了实现这一点,我将结果集的每一行分配给一个HashTable,其中字段名作为“键”,字段中的数据作为“值”。然后,我将整个HashTable结果集分组到单个Vector中,仅作为容器。因此,为了比较两个查询,我实际上正在遍历两个HashTable向量。
我发现这种方法非常适合我,但需要大量内存。由于其他设计要求,我必须通过类似Vector-HashTable的结构进行比较,而不是通过数据库端的过程。
有人有优化建议吗?最佳解决方案应该与我现在正在做的相似,因为大部分代码已经围绕它设计。
谢谢

如果不是同一个数据库,就不能在SQL中完成此操作,除非将所有内容合并到另一个数据库中。 - Daniel Ribeiro
1
我也会使用HashMap和ArrayList,至少可以摆脱旧的集合类的所有同步开销... - John Gardner
感谢所有的建议。到目前为止,大多数人似乎都同意使用ArrayList of HashMaps方法。我的HashMaps通常有大约8个键值对;键大约为10个字符长,值不超过30-50个字符长。比较两个包含10,000个HashMaps的ArrayLists并不罕见。因此,内存中会有20,000个HashMaps。这太多了吗?是否更明智的做法是只加载每个ArrayList的一半,然后处理掉它并加载另一半?唯一的问题是我需要处理和加载四次才能将它们全部比较。 - Tyler
7个回答

6
请将以下内容翻译成中文:

对于两个结果集,请使用相同的“ORDER BY”子句(基于“key”)进行指定。然后,您只需要同时在内存中保留每个结果集中的一条记录。

例如,假设您的结果是res1res2

如果res1key字段小于res2key字段,则res2缺少一些记录;请迭代res1,直到它的key字段等于或大于res2key

同样,如果res1key字段大于res2key字段,则res1缺少一些记录;请改为迭代res2

如果当前记录的key字段相等,则可以比较它们的值,然后迭代两个结果集。

通过这种方式,您可以看到在任何给定时间只需要保留每个结果中的一条记录。


我会选择这样的方式,即“流式”比较,假设您可以通过排序使它们按相同顺序排列。 - John Gardner
问题在于我实际上并不是从两个数据库中提取数据,而是从一个类似数据库的 XML 文档中提取数据,而且我无法控制其中的顺序。 - Tyler
1
然后你自己进行排序,将结果写入临时文件。如果“主内存”不够大,就退而求其次使用大容量存储器。你知道,在50年代和60年代开发的所有那些针对纸张和磁带工作的伟大算法…… - erickson

3

您是否了解享元模式?您是否有许多相等的对象?也许这个模式对于您的“Key”是合适的,因为我想每一行都会重复使用字段名称。如果它们是字符串,您可以调用intern()使它们与其他相等的字符串共享同一个内存位置,因为字符串是不可变的。

另一个可能的优化——不是内存而是速度——如果并发不是问题,那么使用ArrayList而不是Vector可能会更快,因为它们没有同步,所以访问应该会更快。同样,HashMap没有同步,而Hashtable有同步,因此使用前者可能也会更快。


1
intern()要小心——你可能会溢出该区域。 - Thorbjørn Ravn Andersen
@Thorbjørn:我曾经遇到过这个问题,在我的特定情况下增加perm gen解决了它:http://stackoverflow.com/questions/3094925/trying-to-solve-15-puzzle-outofmemoryerror/3095101#3095101 - OscarRyz
1
这是一个永久的解决方案还是如果结果集变得更大,perm gen增量需要进行调整? - Thorbjørn Ravn Andersen
通常有很多相等的对象。我会查看那个模式。关键字名称确实经常重复出现,而且值也是如此。最终匹配的哈希表从未位于相同的内存位置,因此我最终直接比较键-值以找到匹配的表。 - Tyler
@Thorbjørn 大多数情况下都能正常工作(幸运的是我只需要用过三次!:P)但是,当然,您仍然受到物理 RAM 容量的限制。因此,如果您有 1g 的永久代并创建了 2gb 的字符串,则无论如何都会耗尽内存(如果不这样做,您将更早地遇到这种情况)。请参见图表。在这种特殊情况下,字符串重复了很多次,因此需要 25mb 的永久代,而之前的 2gb 非 interned 字符串不足以满足需求。对于差异太大的字符串,这种方法不起作用(就像您指出的那样)。 - OscarRyz

2
你没有说明需要什么类型的比较,但我会通过将行信息转换为单个哈希数字来减少HashMap/Vector所持有的数据量。
类似于这样的内容:
class RowHash {
    private final int id;       // the row id 
    private final int hashCode; // summary of the whole row info 

    public RowHash( ResultSet rs ) {

        this.id = rs.getInt("id");
        // get the strings from all the data 
        this.hashCode = new StringBuilder()
                       .append( rs.getString("field1") )
                       .append( rs.getString("field2") ) 
                       .append(rs.getString("fieldN"))
                       .toString().hashCode();
    }
    public final boolean equals( Object other ) { 
        return this.hashCode() == other.hashCode();
    }
    public final int hasCode() {
       return hashCode;
    }   
} 

然后将其存储到一个ArrayList中,而不是Vector,因为它没有同步处理。
 ... 
 ResulSet rs = ... 
 while( rs.next() ) {
     arrayList.add( new RowHash( rs ) );
 }

这是个想法(具体取决于你需要的比较),计算一个代表整个记录的数字,然后使用该单个数字查看其他查询是否有它。
请记住,这只是一个概念,您需要修改它以适应您的需求。
另一种(可能更简单)减少程序使用大量字符串的内存量的方法是调用intern()
请参阅此answer以比较影响,但实际上取决于您的数据。
这是使用intern在该answer上进行前/后截图。

before

Before

after

之后

蓝色区域表示使用的内存,在第一次使用时为2GB左右,在第二次使用时不到25MB。


很好。我认为其中的某些部分可能会有所帮助,但如果存在某些关键字段匹配,则两个哈希表被视为相同,因此我认为像那样对内容进行哈希处理对我来说行不通。此外,我在程序的其他部分中引用了这些键,因此我需要您建议的哈希以及所有常规键。 - Tyler
是的,如果沒有具體要求,很難回答這個問題。也許你可以先不使用Map( HashMap )來獲取不同行的ID,這樣你將只保存ID而不是把所有的大型數據都存放在內存中,然後只重新獲取那些記錄。再次強調,這取決於您的特定需求。祝你好運! - OscarRyz

1
如果您可以对这两个查询结果进行排序,那么您应该采用排序合并连接算法。

1

你可以封装你自己的对象,例如一个比HashMap更小的'MyRecord',然后它将成为'MyRecord'列表。

如果必须使用HashMap,请使用new HashMap(7,1)而不是默认构造函数,这可以节省内存,因为你说在一个map中有固定的“8个键值对”。


0

如果您没有足够的内存,您将需要外部存储来支持您的数据结构,这很难正确地完成(弱引用映射到您的数据,所有这些都需要滚动到磁盘等),并且在扩展时可能仍然会出现性能问题。

如果您真的有大量的数据,我建议嵌入一个SQL数据库。然后,您可以生成两个包含您的数据的表,并要求数据库查找任何差异,然后删除这些表。我之前使用过Derby,我觉得它不错,但还有其他选择。


0
如果你的数据集不适合内存,那么进行外部排序,然后进行排序合并连接,正如另一个答案中已经指出的那样。
如果你的数据集适合内存,那么只需使用大量内存 - 这是最快的方式。
或者,如果你对特定的优化感兴趣,只是想把你已经做的事情做得更好一点 - 我无法帮助你。

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