Bry*_*n J 20 java memory java-8 java-stream
在查看一些分析结果时,我注意到在紧密循环中使用流(使用而不是另一个嵌套循环)会导致类型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());
}
}
Run Code Online (Sandbox Code Playgroud)
流实施:
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
Run Code Online (Sandbox Code Playgroud)
手动实施:
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
Run Code Online (Sandbox Code Playgroud)
有没有其他人遇到流对象本身消耗内存的问题?/ 这是一个已知的问题?
Tag*_*eev 14
使用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)
Run Code Online (Sandbox Code Playgroud)
所以分配的内存最多的是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)
Run Code Online (Sandbox Code Playgroud)
反汇编实际上表明在预热后不执行迭代器分配.因为转义分析成功告诉JIT迭代器对象没有转义,所以它只是scalarized.如果Iterator
实际分配它将额外需要32个字节:
COUNT AVG SUM DESCRIPTION
1 32 32 java.util.ArrayList$Itr
1 32 (total)
Run Code Online (Sandbox Code Playgroud)
请注意,JIT也可以删除迭代.blackhole
默认情况下你是假的,所以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)
Run Code Online (Sandbox Code Playgroud)
因此,对此特定流的每次调用实际上都会分配200个额外字节.当您执行100_000_000次迭代时,总流版本应比手动版本分配10 ^ 8*200/2 ^ 30 = 18.62Gb更接近您的结果.我想,AtomicLong
里面Random
是标量化为好,但两者Iterator
并AtomicLong
在热身迭代存在(直到JIT实际创建最优化的版本).这可以解释数字中的微小差异.
这个额外的200字节分配不依赖于流大小,而是取决于中间流操作的数量(特别是,每个额外的过滤步骤将增加64 + 24 = 88字节更多).但请注意,这些对象通常是短暂的,可以快速分配,并且可以通过次要GC收集.在大多数现实应用程序中,您可能不必担心这一点.
由于构建Stream API所需的基础结构,不仅存储更多内存.但是,它在速度方面可能会更慢(至少对于这种小输入而言).
有这从甲骨文(这是在俄罗斯,而不是点)开发一个演示,显示一个简单的例子(不是要复杂得多,然后你的),其中执行的速度流的情况下,更糟糕的是30% vs循环.他说这很正常.
有一点我注意到很多人都没有意识到使用Streams(lambda和方法引用更精确)也会创建(可能)很多你不会知道的类.
尝试运行您的示例:
-Djdk.internal.lambda.dumpProxyClasses=/Some/Path/Of/Yours
Run Code Online (Sandbox Code Playgroud)
并查看您的代码将创建多少其他类以及Streams需要的代码(通过ASM)
归档时间: |
|
查看次数: |
7423 次 |
最近记录: |