【发布时间】:2016-12-19 20:51:31
【问题描述】:
在查看一些分析结果时,我注意到在紧密循环中使用流(使用而不是另一个嵌套循环)会导致 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
有没有其他人遇到过流对象本身消耗内存的问题? / 这是一个已知问题吗?
【问题讨论】:
-
是的,不,这完全可以预料。对于这么小的输入,流的开销肯定会很大。
-
不完全相关,但不等于
getIndexOfNothingManualImpl是return nums.stream().anyMatch(predicate)? -
我很有信心,
for循环在底层创建了一个Iterator实现。不知何故,您的分析器错过了…… -
有趣的是,使用“手动”实现(阅读:基于
Iterator的实现)运行应该创建了更多Random实例,但是显着更少的ArrayLists。你怎么能相信这样的数字? -
顺便说一句,JVM 检测到您的
blackHole变量始终为false是没有问题的。由于它没有声明volatile,优化器不必考虑来自其他线程的更新,并且在您的顺序代码路径中,它不可能转到true。
标签: java memory java-8 java-stream