Ответ 1
Используя Stream API, вы действительно выделяете больше памяти, хотя ваша экспериментальная установка несколько сомнительна. Я никогда не использовал JFR, но мои результаты с использованием JOL очень похожи на ваши.
Обратите внимание, что вы измеряете не только кучу, выделенную во время запроса ArrayList
, но и во время ее создания и совокупности. Распределение во время распределения и совокупности одиночного ArrayList
должно выглядеть следующим образом (64 бит, сжатые ООП, через JOL):
COUNT AVG SUM DESCRIPTION
1 416 416 [Ljava.lang.Object;
1 24 24 java.util.ArrayList
1 32 32 java.util.Random
1 24 24 java.util.concurrent.atomic.AtomicLong
4 496 (total)
Таким образом, наибольшая выделенная память представляет собой массив Object[]
, используемый внутри ArrayList
для хранения данных. AtomicLong
является частью реализации класса Random. Если вы выполняете это 100_000_000 раз, тогда вы должны иметь как минимум 496*10^8/2^30 = 46.2 Gb
, выделенный в обоих тестах. Тем не менее эта часть может быть пропущена, поскольку она должна быть одинаковой для обоих тестов.
Еще одна интересная вещь - встраивание. JIT достаточно умен, чтобы встроить все getIndexOfNothingManualImpl
(через java -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining StreamMemoryTest
):
StreamMemoryTest::main @ 13 (59 bytes)
...
@ 30 StreamMemoryTest::getIndexOfNothingManualImpl (43 bytes) inline (hot)
@ 1 java.util.ArrayList::iterator (10 bytes) inline (hot)
\-> TypeProfile (2132/2132 counts) = java/util/ArrayList
@ 6 java.util.ArrayList$Itr::<init> (6 bytes) inline (hot)
@ 2 java.util.ArrayList$Itr::<init> (26 bytes) inline (hot)
@ 6 java.lang.Object::<init> (1 bytes) inline (hot)
@ 8 java.util.ArrayList$Itr::hasNext (20 bytes) inline (hot)
\-> TypeProfile (215332/215332 counts) = java/util/ArrayList$Itr
@ 8 java.util.ArrayList::access$100 (5 bytes) accessor
@ 17 java.util.ArrayList$Itr::next (66 bytes) inline (hot)
@ 1 java.util.ArrayList$Itr::checkForComodification (23 bytes) inline (hot)
@ 14 java.util.ArrayList::access$100 (5 bytes) accessor
@ 28 StreamMemoryTest$$Lambda$1/791452441::test (8 bytes) inline (hot)
\-> TypeProfile (213200/213200 counts) = StreamMemoryTest$$Lambda$1
@ 4 StreamMemoryTest::lambda$main$0 (13 bytes) inline (hot)
@ 1 java.lang.Integer::intValue (5 bytes) accessor
@ 8 java.util.ArrayList$Itr::hasNext (20 bytes) inline (hot)
@ 8 java.util.ArrayList::access$100 (5 bytes) accessor
@ 33 StreamMemoryTest::consume (19 bytes) inline (hot)
Разборка на самом деле показывает, что после разминки не выполняется выделение итератора. Поскольку анализ escape успешно сообщает JIT, что объект итератора не исчезает, он просто сканируется. Если бы на самом деле было выделено Iterator
, то дополнительно 32 байта:
COUNT AVG SUM DESCRIPTION
1 32 32 java.util.ArrayList$Itr
1 32 (total)
Обратите внимание, что JIT также может удалить итерацию вообще. По умолчанию ваш blackhole
является ложным, поэтому blackhole = blackhole && value
не меняет его независимо от value
, а вычисление value
вообще может быть исключено, так как оно не имеет побочных эффектов. Я не уверен, действительно ли это произошло (чтение разборки для меня довольно сложно), но это возможно.
Однако в то время как getIndexOfNothingStreamImpl
также, кажется, встроен во внутреннее пространство, анализ escape не выполняется, поскольку в потоковом API слишком много взаимозависимых объектов, поэтому происходят фактические распределения. Таким образом, он действительно добавляет пять дополнительных объектов (таблица составлена вручную из выходов JOL):
COUNT AVG SUM DESCRIPTION
1 32 32 java.util.ArrayList$ArrayListSpliterator
1 24 24 java.util.stream.FindOps$FindSink$OfRef
1 64 64 java.util.stream.ReferencePipeline$2
1 24 24 java.util.stream.ReferencePipeline$2$1
1 56 56 java.util.stream.ReferencePipeline$Head
5 200 (total)
Таким образом, каждый вызов этого конкретного потока фактически выделяет 200 дополнительных байтов. Поскольку вы выполняете итерации 100_000_000, в общей версии Stream должна выделяться 10 ^ 8 * 200/2 ^ 30 = 18.62Gb больше, чем ручная версия, близкая к вашему результату. Я думаю, что AtomicLong
внутри Random
также сканируется, но как теги Iterator
, так и AtomicLong
присутствуют во время итераций разминки (пока JIT фактически не создаст самую оптимизированную версию). Это объясняет незначительные расхождения в числах.
Это дополнительное 200-байтовое распределение не зависит от размера потока, но зависит от количества операций промежуточного потока (в частности, каждый дополнительный шаг фильтра добавит 64 + 24 = 88 байт). Однако обратите внимание, что эти объекты обычно недолговечны, быстро распределяются и могут быть собраны небольшим GC. В большинстве реальных приложений вам, вероятно, не стоит беспокоиться об этом.