Могут ли потоки 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.