在查看一些分析结果时,我注意到在紧密循环(使用而不是另一个嵌套循环)中使用流会导致类型java.util.stream.ReferencePipeline
和java.util.ArrayList$ArrayListSpliterator
的对象产生大量内存开销。我将有问题的流转换为foreach循环,并且内存消耗显着下降。
我知道溪流没有做出比普通环更好的承诺,但我的印象是差异可以忽略不计。在这种情况下,它似乎增加了40%。
这是我编写的用于隔离问题的测试类。我使用JFR监视内存消耗和对象分配:
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.function.Predicate;
public class StreamMemoryTest {
private static boolean blackHole = false;
public static List<Integer> getRandListOfSize(int size) {
ArrayList<Integer> randList = new ArrayList<>(size);
Random rnGen = new Random();
for (int i = 0; i < size; i++) {
randList.add(rnGen.nextInt(100));
}
return randList;
}
public static boolean getIndexOfNothingManualImpl(List<Integer> nums, Predicate<Integer> predicate) {
for (Integer num : nums) {
// Impossible condition
if (predicate.test(num)) {
return true;
}
}
return false;
}
public static boolean getIndexOfNothingStreamImpl(List<Integer> nums, Predicate<Integer> predicate) {
Optional<Integer> first = nums.stream().filter(predicate).findFirst();
return first.isPresent();
}
public static void consume(boolean value) {
blackHole = blackHole && value;
}
public static boolean result() {
return blackHole;
}
public static void main(String[] args) {
// 100 million trials
int numTrials = 100000000;
System.out.println("Beginning test");
for (int i = 0; i < numTrials; i++) {
List<Integer> randomNums = StreamMemoryTest.getRandListOfSize(100);
consume(StreamMemoryTest.getIndexOfNothingStreamImpl(randomNums, x -> x < 0));
// or ...
// consume(StreamMemoryTest.getIndexOfNothingManualImpl(randomNums, x -> x < 0));
if (randomNums == null) {
break;
}
}
System.out.print(StreamMemoryTest.result());
}
}
流实施:
Memory Allocated for TLABs 64.62 GB
Class Average Object Size(bytes) Total Object Size(bytes) TLABs Average TLAB Size(bytes) Total TLAB Size(bytes) Pressure(%)
java.lang.Object[] 415.974 6,226,712 14,969 2,999,696.432 44,902,455,888 64.711
java.util.stream.ReferencePipeline$2 64 131,264 2,051 2,902,510.795 5,953,049,640 8.579
java.util.stream.ReferencePipeline$Head 56 72,744 1,299 3,070,768.043 3,988,927,688 5.749
java.util.stream.ReferencePipeline$2$1 24 25,128 1,047 3,195,726.449 3,345,925,592 4.822
java.util.Random 32 30,976 968 3,041,212.372 2,943,893,576 4.243
java.util.ArrayList 24 24,576 1,024 2,720,615.594 2,785,910,368 4.015
java.util.stream.FindOps$FindSink$OfRef 24 18,864 786 3,369,412.295 2,648,358,064 3.817
java.util.ArrayList$ArrayListSpliterator 32 14,720 460 3,080,696.209 1,417,120,256 2.042
手动实施:
Memory Allocated for TLABs 46.06 GB
Class Average Object Size(bytes) Total Object Size(bytes) TLABs Average TLAB Size(bytes) Total TLAB Size(bytes) Pressure(%)
java.lang.Object[] 415.961 4,190,392 10,074 4,042,267.769 40,721,805,504 82.33
java.util.Random 32 32,064 1,002 4,367,131.521 4,375,865,784 8.847
java.util.ArrayList 24 14,976 624 3,530,601.038 2,203,095,048 4.454
有没有其他人遇到流对象本身消耗内存的问题? /这是一个已知问题吗?
答案 0 :(得分:12)
使用Stream API确实可以分配更多内存,尽管您的实验设置有些疑问。我从未使用过JFR,但我使用JOL的发现与你的相似。
请注意,您不仅要测量在ArrayList
查询期间分配的堆,还要测量在创建和填充期间分配的堆。单个ArrayList
的分配和填充期间的分配应该如下所示(64位,压缩OOP,通过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)
反汇编实际上表明在预热后不执行迭代器分配。因为转义分析成功地告诉JIT迭代器对象没有转义,所以它只是简化了scalarized。如果实际分配了Iterator
,则需要额外的32个字节:
COUNT AVG SUM DESCRIPTION
1 32 32 java.util.ArrayList$Itr
1 32 (total)
请注意,JIT也可以删除迭代。默认情况下,您的blackhole
为false,因此无论blackhole = blackhole && value
如何,value
都不会更改,value
计算可能完全排除,因为它没有副作用。我不确定它是否真的这样做(阅读反汇编对我来说很难),但这是可能的。
然而,虽然getIndexOfNothingStreamImpl
似乎内联所有内容,但转义分析失败,因为流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次迭代时,总流版本应比手动版本分配10 ^ 8 * 200/2 ^ 30 = 18.62Gb更接近您的结果。我认为,AtomicLong
内Random
也是标量化的,但在预热迭代期间都存在Iterator
和AtomicLong
(直到JIT实际创建最优化的版本)。这可以解释数字中的微小差异。
这个额外的200字节分配不依赖于流大小,而是取决于中间流操作的数量(特别是,每个额外的过滤步骤将增加64 + 24 = 88字节更多)。但请注意,这些对象通常是短暂的,可以快速分配,并且可以通过次要GC收集。在大多数现实应用程序中,您可能不必担心这一点。
答案 1 :(得分:6)
由于构建Stream API所需的基础结构,不仅内存更多。但是,可能在速度方面要慢一些(至少对于这个小输入而言)。
来自Oracle的一位开发人员的this演示文稿(这是俄语,但这不是重点)显示了一个简单的例子(并不比你的复杂得多),其执行速度是在Streams vs Loops的情况下,差30%。他说这很正常。
有一点我注意到很多人都没有意识到使用Streams(lambda&method和方法引用更精确)也会创建(可能)很多类,你会不知道。
尝试使用以下命令运行您的示例:
-Djdk.internal.lambda.dumpProxyClasses=/Some/Path/Of/Yours
看看您的代码和Streams需要的代码(通过ASM)将创建多少其他类