2012-03-06 3 views
9

Я пытаюсь прочитать большой текстовый корпус в памяти с помощью Java. В какой-то момент он попадает в стену, и только мусор собирается бесконечно. Я хотел бы знать, есть ли у кого-либо опыт избиения GC Java в представлении с большими наборами данных.Плохая производительность с большими списками Java

Я читаю 8-гигабайтный файл английского текста в UTF-8 с одним предложением к строке. Я хочу split() каждую строку в пробеле и хранить полученные массивы String в ArrayList<String[]> для дальнейшей обработки. Вот упрощенная программа, которая проявляет проблему:

/** Load whitespace-delimited tokens from stdin into memory. */ 
public class LoadTokens { 
    private static final int INITIAL_SENTENCES = 66000000; 

    public static void main(String[] args) throws IOException { 
     List<String[]> sentences = new ArrayList<String[]>(INITIAL_SENTENCES); 
     BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 
     long numTokens = 0; 
     String line; 

     while ((line = stdin.readLine()) != null) { 
      String[] sentence = line.split("\\s+"); 
      if (sentence.length > 0) { 
       sentences.add(sentence); 
       numTokens += sentence.length; 
      } 
     } 
     System.out.println("Read " + sentences.size() + " sentences, " + numTokens + " tokens."); 
    } 
} 

Похоже, что вырезано-высушено, правильно? Вы заметите, что я даже сделал предварительный размер ArrayList; У меня чуть меньше 66 миллионов предложений и 1,3 миллиарда жетонов. Теперь, если вы выскочить ваш Java object sizes ссылку и карандаш, вы обнаружите, что должно требовать около:

  • 66e6 String[] ссылки @ 8 байт еа = 0,5 GB
  • 66e6 String[] объекты @ 32 байт еа = 2 ГБ
  • 66e6 char[] объекты @ 32 байта еа = 2 Гб
  • 1.3e9 String ссылки @ 8 байт еа = 10 ГБ
  • 1.3e9 String с @ 44 байт еа = 53 Гб
  • 8e9 char с @ 2 байта еа = 15 Гб

83 Гб. (Вы заметите, что мне действительно нужно использовать размеры 64-битных объектов, поскольку Compressed OOPs не может помочь мне с кучей 32 ГБ.) Нам повезло, что у вас есть машина RedHat 6 со 128 ГБ оперативной памяти, поэтому я запускаю моя 64-разрядная серверная виртуальная машина Java HotSpot ™ (сборка 20.4-b02, смешанный режим) из моего набора Java SE 1.6.0_29 с pv giant-file.txt | java -Xmx96G -Xms96G LoadTokens только для того, чтобы быть в безопасности и отбрасывать назад, пока я смотрю top.

Где-то менее чем на полпути через вход, около 50-60 ГБ RSS, параллельный сборщик мусора запускает до 1300% CPU (16 proc box) и считывает остановки хода. Затем идет еще несколько ГБ, затем прогресс останавливается еще дольше. Он заполняет 96 ГБ и еще не завершен. Я отпустил его на полтора часа, и это просто сжигание ~ 90% времени в системе GC. Это кажется экстремальным.

Чтобы убедиться, что я не был сумасшедшим, я взломал эквивалентный Python (все две строки;), и он продлился до 12 минут и 70 ГБ RSS.

Итак: я делаю что-то немое? (Помимо неэффективного способа хранения вещей, который я действительно не могу вам помочь, и даже если мои структуры данных толстые, пока они подходят, Java не должна просто задыхаться.) Есть ли волшебство Рекомендации GC для действительно больших куч? Я попробовал -XX:+UseParNewGC, и это кажется еще хуже.

+0

Где хранятся объекты 'char []', поддерживающие строки? –

+0

В объектах 'String': 24-байтовый заголовок объекта + 8 байтов' char [] 'указатель + 4 байта start, offset и hashcode, если мои вычисления верны. –

+0

Это 'char []' * reference * - но как насчет 'char []' * objects * самих? У массива 'char []' тоже есть надстройка объекта ... –

ответ

3

-XX:+UseConcMarkSweepGC: заканчивается на 78 ГБ и ~ 12 минут. (Почти так же хорошо, как Python!) Спасибо за помощь каждого.

+0

Я часто использую CMS для Java-сервера с большой кучей, чтобы уменьшить влияние gc на время отклика. Я не был уверен, что изменение политики поможет вашему коду в такой задаче. Думаю, использование CMS изменило способ разбивки кучи на части, и ваша JVM получает более крупный OldGen. –

2

Идея 1

Старт с учетом этого:

while ((line = stdin.readLine()) != null) { 

Это, по крайней мере используется быть так, что readLine возвратит String с поддержкой char[] не менее 80 символов.Независимо от того или нет, что становится проблемой, зависит от того, что делает следующая строка:

String[] sentence = line.split("\\s+"); 

Вы должны определить, являются ли строки, возвращаемые split держать ту же поддержку char[].

Если они делают (и предполагая, ваши линии часто короче, чем 80 символов), вы должны использовать:

line = new String(line); 

Это создаст клон копию строки с «правой размера» массива строк

Если они сделать не, то вы должны потенциально выработать какой-то способ создания такого же поведения, но изменить его так, что они делают использовать ту же поддержку char[] (т.е. они подстроки исходной линии) - и выполняют ту же операцию клонирования , конечно. Вы не хотите отдельного char[] за каждое слово, так как это потеряет гораздо больше памяти, чем пространства.

Идея 2

Ваше название говорит о плохой работе списков - но, конечно, вы можете легко взять список из уравнения здесь, просто создавая String[][], по крайней мере, для целей тестирования. Похоже, вы уже знаете размер файла - и если вы этого не сделаете, вы можете запустить его через wc, чтобы проверить заранее. Просто чтобы узнать, можете ли вы избежать этой проблемы , чтобы начать с.

Идея 3

Сколько различных слов в вашем корпусе? Вы считали сохранение HashSet<String> и добавление к нему каждого слова, когда вы сталкиваетесь с ним? Таким образом, вы, скорее всего, окажетесь с далеко меньше строк. На данный момент вы, вероятно, захотите отказаться от «одиночной поддержки char[] за строку» от первой идеи - вы бы хотели бы каждой строкой, которая будет поддерживаться собственным массивом символов, так как иначе строка с одним новым словом в по-прежнему требуется много символов. (В качестве альтернативы, для реальной тонкой настройки, вы можете увидеть, сколько «новые слова» есть в строке и клонировать каждую строку или нет.)

+0

Re: Идея 3, вы можете использовать 'String.intern()'? –

+0

@LouisWasserman: Потенциально - но только в том случае, если процесс не будет делать ничего другого. Обычно я предпочитаю иметь свой собственный интернированный набор, чтобы избежать «загрязнения» всего процесса. (Хотя могут быть фанковые вещи, которые означают, что в наши дни это не проблема. Он просто чувствует себя «чище».) –

+2

Хммм. Альтернативное предложение - Guava ['Interners.newWeakInterner'] (http://docs.guava-libraries.googlecode.com/git-history/release/javadoc/com/google/common/collect/Interners.html#newWeakInterner()), чтобы сделать это со слабыми ссылками, так что интернированные строки могут получить GC'd, когда вы закончите. –

2

Вы должны использовать следующие приемы:

  • Помощь JVM для сбора одних и тех же токенов в одну строковую ссылку благодаря sentences.add(sentence.intern()). См. String.intern. Насколько я знаю, он должен также иметь эффект, о котором говорил Джон Скит, он разрезает массив char на мелкие кусочки.

  • Использование experimental HotSpot options компактной строки и полукокс [] реализации и связанных с них:

    -XX:+UseCompressedStrings -XX:+UseStringCache -XX:+OptimizeStringConcat 
    

С количеством таких памятей, вы должны настроить систему и JVM для use large pages.

Это действительно сложно улучшить производительность при настройке GC только и более 5%.Прежде всего, вы должны сократить потребление памяти приложения благодаря профилированию.

Кстати, мне интересно, действительно ли вам нужно получить полное содержание книги в памяти - я не знаю, что ваш код делает со всеми предложениями, но вы должны рассмотреть альтернативный вариант, например Lucene indexing tool, для подсчета слов или извлечение любой другой информации из вашего текста.

+0

Спасибо за предложения. Я пробовал использовать String в предыдущих приложениях; он становится очень медленным с большим количеством данных, и для этого требуется огромный PermGen, который действительно смущает GC. Я попробовал варианты оптимизации String, и, возможно, это немного снизило использование памяти, но в конечном итоге оно заполняет память и пробки. Идея больших страниц - хорошая. к сожалению, вам действительно нужно перезагрузиться, чтобы получить достаточно непрерывную свободную память (что это, DOS?;), и эта память не может использоваться ни для чего другого. Я читаю настройки на GC, и я думаю, что я собираюсь попробовать параллельный коллекционер. –

0

Вы должны проверить, как ваше пространство кучи разделено на части (PermGen, OldGen, Eden и Survivors) благодаря VisualGC, который теперь является плагином для VisualVM.

В вашем случае, вы, вероятно, хотите, чтобы уменьшить Иден и Выживших, чтобы увеличить OldGen, чтобы ваш GC не раскручивается в сборе полный OldGen ...

Чтобы сделать это, вы должны использовать дополнительные опции, как :

-XX:NewRatio=2 -XX:SurvivorRatio=8 

Остерегайтесь этих зон, и их политика распределения по умолчанию зависит от используемого вами коллекционера. Так измените один параметр за раз и проверьте еще раз.

Если все, что String должно содержать в памяти всю жизнь JVM, это хорошая идея для их интернализации в PermGen, достаточно большой, с -XX:MaxPermSize и избежать сбора в этой зоне благодаря -Xnoclassgc.

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

-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:verbosegc.log 
+0

Я смотрел на это, и я мог бы попробовать. Спасибо за предложение. –