Java: заполнение отсортированных в памяти пакетов - PullRequest
3 голосов
/ 26 июня 2011

Таким образом, я использую Java для выполнения многократного внешнего слияния больших дисковых файлов с разделителями строк.Пакеты кортежей считываются в TreeSet, которые затем выгружаются в отсортированные на диске партии.Как только все данные исчерпаны, эти партии затем сортируются слиянием на выходе.

В настоящее время я использую магические числа, чтобы выяснить, сколько кортежей мы можем поместить в память.Это основано на статической фигуре, показывающей, как могут приблизительно соответствовать кортежи на МБ пространства кучи, и сколько пространства кучи доступно, используя:

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

Однако это не всегда работает так хорошо, так как мыможет быть сортировка кортежей разной длины (для которых статическая фигура кортеж на мегабайт может быть слишком консервативной), и теперь я хочу использовать шаблоны в полусреднем весе для большего количества джемов, что может сделать фигуру еще более изменчивой.

Так что я ищу лучший способ заполнить пространство кучи до краев.В идеале решение должно быть:

  • надежным (без риска исключений кучи)
  • гибким (не основанным на статических числах)
  • эффективным (например, неопрос оценки памяти во время выполнения после каждого кортежа)

Есть идеи?

Ответы [ 4 ]

2 голосов
/ 26 июня 2011

Заполнение кучи до краев может быть плохой идеей из-за мусора сборщика мусора. (Поскольку память почти заполнена, эффективность сборки мусора приближается к 0, поскольку усилия по сбору зависят от размера кучи, но объем освобожденной памяти зависит от размера объектов, определенных как недоступные).

Однако, если вы должны, вы не можете просто сделать это следующим образом?

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

Призывы к обнаружению свободной памяти будут редкими, поэтому не стоит сильно снижать производительность. Например, если у вас есть 1 ГБ пространства кучи, а 1 МБ оставлено пустым, а MAX_RECORD_SIZE - это десятикратный средний размер записи, getFreeSpace() будет вызываться просто как log (1000) / -log (0.9) ~ = 66 раз.

2 голосов
/ 26 июня 2011

Зачем беспокоиться о расчете, сколько предметов вы можете держать? Как насчет того, чтобы позволить 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 байт. Таким образом, когда вы закончите чтение, вы освободите до 4 Кб памяти. После этого ваши дополнительные потребности в памяти будут минимальными. Вам нужно достаточно памяти для создания итератора для набора, давайте будем щедрыми и предположим, 200 байтов. Вам также понадобится память для хранения строкового вывода ваших кортежей (но только временно). Вы говорите, что кортежи содержат около 200 символов. Давайте удвоим это, чтобы учесть разделители - 400 символов, что составляет 800 байт. Так что все, что вам действительно нужно, это дополнительные 1 Кбайт. Так что вы в порядке, так как вы только что выпустили 4k байтов.

Причина, по которой вам не нужно беспокоиться о памяти, используемой для хранения строкового вывода ваших кортежей, заключается в том, что они недолговечны и упоминаются только в выходных данных цикла. Обратите внимание, что Writer скопирует содержимое в свой буфер и затем отбросит строку. Таким образом, при следующем запуске сборщика мусора память может быть восстановлена.

Я проверил, и OOME в add не оставит TreeSet в несогласованном состоянии, и выделение памяти для нового Entry (внутренняя реализация для хранения пары ключ / значение) произойдет до того, как внутреннее представление изменено.

1 голос
/ 26 июня 2011

Вы можете действительно заполнить кучу до краев, используя прямую запись в память (она существует в Java!). Это в sun.misc.Unsafe, но на самом деле не рекомендуется для использования. Смотрите здесь для более подробной информации. Возможно, я бы посоветовал написать код JNI и использовать существующие алгоритмы C ++.

0 голосов
/ 27 июня 2011

Я добавлю это как идею, с которой я играл, используя 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. В противном случае никакие ограничения не накладываются на время, в которое будет очищена мягкая ссылка, или порядок, в котором будет очищен набор таких ссылок на разные объекты. Реализации виртуальных машин, однако, рекомендуется отклонять от очистки недавно созданных или недавно использованных программных ссылок.

Хотели бы услышать отзывы, которые кажутся мне элегантным решением, хотя поведение может варьироваться между виртуальными машинами?

Тестируя на своем ноутбуке, я обнаружил, что софт-ссылка очищается нечасто, но иногда очищается слишком рано, поэтому я думаю объединить ее с ответом меритон:

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]);
      }
   }
}

Опять мысли приветствуются!

...