Могут ли потоки Java 8 вызывать уменьшение O (1) памяти на неограниченных данных, чтобы стать памятью O (n) из-за базовой реализации ForkJoin
Я написал реализацию потоков, которая выполняет четыре простых сокращения (+ и <) в строках файла.
Сначала я выполнил четыре потока, но я решил написать свой собственный накопитель и объединитель, чтобы я мог выполнять все четыре сокращения в одном потоке. На небольших наборах данных (10 000 000 строк) это сокращает время выполнения примерно до 1/4, как и ожидалось, и работает через 14 секунд на моем оборудовании.
fileIn = new BufferedReader(new InputStreamReader(
new URL(args[0].trim()).openStream()));
final Results results = fileIn.lines()
.parallel()
.skip(1)
.map(User::parse)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Results::new, Results::accumulate, Results::combine);
Results::accumulate
и Results::combine
корректно объединяют пользователей в результаты и результаты с результатами соответственно, и эта реализация работает
отлично подходит для небольших наборов данных.
Я попытался использовать .reduce()
, а результаты аналогичны, но я попытался .collect()
уменьшить создание короткоживущих объектов.
Проблема в том, что когда я использую данные реального мира с 1 миллиардом строк, я сталкиваюсь с проблемой, которая говорит о том, что потоки Java 8 неспособны выполнить эту задачу. Память кучи наблюдается в JConsole, чтобы подняться до выделенного 12 ГБ примерно линейно, а затем OOM.
У меня создалось впечатление, что сборщик или редуктор обеспечит производительность, сравнимую с итеративным решением, которое должно быть ограничено процессором и IO, но не памятью, потому что шаг восстановления дает результат, который не растет, это сокращение
Когда я беру кучу кучи и помещаю его в jhat, я вижу, что около 7 ГБ заняты строками, и эти строки могут быть четко видны как строки входного файла. Я чувствую, что они не должны быть в памяти, но jhat показывает очень большую связанную с ForkJoin структуру, которая накапливается в памяти:
Static reference from java.util.concurrent.ForkJoinPool.common (from class java.util.concurrent.ForkJoinPool) :
--> [email protected] (76 bytes) (field workQueues:)
--> [Ljava.util.concurrent.ForkJoinPool$WorkQueue;@0x786eda598 (144 bytes) (Element 3 of [Ljava.util.concurrent.ForkJoinPool$WorkQueue;@0x786eda598:)
--> [email protected] (96 bytes) (field currentSteal:)
--> [email protected] (130 bytes) (field completer:)
--> [email protected] (130 bytes) (field completer:)
--> [email protected] (130 bytes) (field leftChild:)
--> [email protected] (130 bytes) (field localResult:)
--> [email protected] (53 bytes) (field spine:)
--> [[Ljava.lang.Object;@0x7b25ffe48 (144 bytes) (Element 12 of [[Ljava.lang.Object;@0x7b25ffe48:)
--> [Ljava.lang.Object;@0x7b37c4f20 (262160 bytes) (Element 19598 of [Ljava.lang.Object;@0x7b37c4f20:)
--> 31ea87ba876505645342b31928394b3c,2013-11-24T23:02:17+00:00,898,22200,1314,700 (28 bytes) (field value:)
--> [[email protected] (170 bytes) // <<<< There are thousands of these
В ApplicationShutdownHooks, локальных ссылках и системных классах есть другие ссылки, но я показываю суть проблемы, и это заставляет память расти O (n), когда
Реализует ли реализация потоков эту O (1) память O (n) памяти, удерживая все строки в классах ForkJoin? Я люблю потоки, и я не хочу, чтобы это было так: (
Ответы
Ответ 1
Спасибо Марко Топольнику и Хольгеру за то, что он пришел к правильному ответу. Хотя ни один из них не ответил на мой вопрос, поэтому я постараюсь связать это для потомков:)
.skip(1)
является очень дорогим в параллельном потоке, потому что он требует упорядочения, чтобы пропустить точно первую запись, в соответствии с Javadoc для Stream.skip( )
Считывая первую строку BufferedReader перед вызовом .lines()
, он успешно пропускает первую строку в моей реализации.
Затем удаление .skip()
решает проблему памяти, и в JConsole наблюдается хороший отскок и возвращение в < 1 ГБ на каждую сборку мусора, даже если программа обрабатывает 1 миллиард строк. Это желаемое поведение и достаточно близко для памяти O (1) для моих целей.
В отличие от вышеприведенного предложения относительные местоположения .parallel()
и .skip(1)
не имеют значения, вы не можете переупорядочить их, чтобы сделать .skip(1)
"до" .parallel().
. Рисунок построителя предполагает, что порядок важен, и он предназначен для других промежуточных операций, но не для этого. Я помню эту тонкость из моих сертификационных материалов OCP, но, похоже, она не находится в Javadoc, поэтому ссылки не упоминаются. Тем не менее, я подтвердил это экспериментально, сделав изолированное изменение и наблюдая регрессию в JConsole и связанный OOM.