Java:填充内存中已排序的批次

时间:2011-06-26 15:43:46

标签: java memory-management heap-memory

所以我使用Java来做多行外部合并的大型磁盘文件的行分隔元组。将批量元组读入TreeSet,然后将其转储到磁盘上的已分类批次中。一旦所有数据都用尽,这些批次就会合并排序到输出中。

目前,我正在使用幻数来确定我们可以在内存中容纳多少元组。这是基于一个静态图,表明元组可以大致适合每MB堆空间,以及使用多少堆空间:

long max = Runtime.getRuntime().maxMemory();
long used = Runtime.getRuntime().totalMemory();
long free = Runtime.getRuntime().freeMemory();      
long space = free + (max - used);

然而,这并不总是那么好用,因为我们可能会对不同长度的元组进行排序(对于每个MB的静态元组数据可能过于保守),我现在想要使用flyweight模式来更多地堵塞,这可能会使数字变得更加可变。

所以我正在寻找一种更好的方法来填充堆积空间。理想情况下,解决方案应该是:

  • 可靠(没有堆空间异常的风险)
  • 灵活(不基于静态数字)
  • 高效(例如,不在每个元组之后轮询运行时内存估计值)

有什么想法吗?

4 个答案:

答案 0 :(得分:2)

为什么要计算可容纳的物品数量?当你用尽所有记忆,捕捉异常并继续时,让java告诉你怎么样?例如,

    // prepare output medium now so we don't need to worry about having enough 
    // memory once the treeset has been filled.
    BufferedWriter writer = new BufferedWriter(new FileWriter("output"));

    Set<?> set = new TreeSet<?>();
    int linesRead = 0;
    {
        BufferedReader reader = new BufferedReader(new FileReader("input"));
        try {
            String line = reader.readLine();
            while (reader != null) {
                set.add(parseTuple(line));
                linesRead += 1;
                line = reader.readLine();
            }
            // end of file reached
            linesRead = -1;
        } catch (OutOfMemoryError e) {
            // while loop broken
        } finally {
            reader.close();
        }
        // since reader and line were declared in a block their resources will 
        // now be released 
    }

    // output treeset to file
    for (Object o: set) {
        writer.write(o.toString());
    }
    writer.close();

    // use linesRead to find position in file for next pass
    // or continue on to next file, depending on value of linesRead

如果您仍然遇到内存问题,只需将阅读器的缓冲区设置得特别大,以便保留更多内存。

BufferedReader中缓冲区的默认大小为4096字节。因此,在完成阅读时,您将释放超过4k的内存。在此之后,您的额外内存需求将是最小的。你需要足够的内存来为集合创建一个迭代器,让我们慷慨并假设200字节。您还需要内存来存储元组的字符串输出(但只是暂时存储)。你说元组包含大约200个字符。让我们加倍考虑分隔符--400个字符,即800个字节。所以你真正需要的是额外的1k字节。所以你很好,因为你刚刚发布了4k字节。

您不必担心用于存储元组的字符串输出的内存的原因是因为它们是短暂的并且仅在输出for循环中引用。请注意,Writer会将内容复制到其缓冲区中,然后丢弃该字符串。因此,下次垃圾收集器运行时,可以回收内存。

我已经检查了,add中的OOME不会使TreeSet处于不一致状态,并且新Entry的内存分配(用于存储键/值对的内部实现) )在内部表示被修改之前发生。

答案 1 :(得分:2)

由于垃圾收集器捣乱,将堆填充到边缘可能是一个坏主意。 (当内存几乎已满时,垃圾收集的效率接近0,因为收集的工作量取决于堆大小,但释放的内存量取决于被识别为无法访问的对象的大小。)

但是,如果必须,您不能简单地按照以下方式进行操作吗?

for (;;) {
    long freeSpace = getFreeSpace();
    if (freeSpace < 1000000) break;
    for (;;freeSpace > 0) {
        treeSet.add(readRecord());
        freeSpace -= MAX_RECORD_SIZE;
    }
}

发现免费记忆的呼声很少,所以不应该对性能征税。例如,如果你有1 GB的堆空间,并且1MB为空,而MAX_RECORD_SIZE是平均记录大小的十倍,那么getFreeSpace()将被调用仅仅是log(1000)/ -log(0.9)〜 = 66次。

答案 2 :(得分:1)

你可以使用直接内存写入将堆填充到边缘(它确实存在于Java中!)。它位于sun.misc.Unsafe,但实际上并不推荐使用。有关详细信息,请参阅here。我可能会建议编写一些JNI代码,并使用现有的C ++算法。

答案 3 :(得分:0)

我将此添加为我正在玩的一个想法,涉及使用SoftReference作为“嗅探器”来实现低内存。

SoftReference<Byte[]> sniffer = new SoftReference<String>(new Byte[8192]);
while(iter.hasNext()){
   tuple = iter.next();
   treeset.add(tuple);
   if(sniffer.get()==null){
      dump(treeset);
      treeset.clear();
      sniffer = new SoftReference<String>(new Byte[8192]);
   }
}

这在理论上可能很有效,但我不知道SoftReference的确切行为。

  

在虚拟机抛出OutOfMemoryError之前,保证已清除对软可访问对象的所有软引用。否则,不会对清除软引用的时间或清除对不同对象的一组此类引用的顺序施加约束。但是,鼓励虚拟机实现偏向清除最近创建或最近使用的软引用。

我希望听到反馈,因为在我看来这是一个优雅的解决方案,尽管虚拟机之间的行为可能会有所不同?

在我的笔记本电脑上进行测试时,我发现软件参考很少被清除,但有时会过早清除,所以我想把它与meriton的答案结合起来:

SoftReference<Byte[]> sniffer = new SoftReference<String>(new Byte[8192]);
while(iter.hasNext()){
   tuple = iter.next();
   treeset.add(tuple);
   if(sniffer.get()==null){
      free = MemoryManager.estimateFreeSpace();
      if(free < MIN_SAFE_MEMORY){
         dump(treeset);
         treeset.clear();
         sniffer = new SoftReference<String>(new Byte[8192]);
      }
   }
}

再次,欢迎您的想法!