Java 8流对象占用大量内存

时间:2016-12-19 20:51:32

标签: java memory java-8 java-stream

在查看一些分析结果时,我注意到在紧密循环(使用而不是另一个嵌套循环)中使用流会导致类型java.util.stream.ReferencePipelinejava.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

有没有其他人遇到流对象本身消耗内存的问题? /这是一个已知问题吗?

2 个答案:

答案 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更接近您的结果。我认为,AtomicLongRandom也是标量化的,但在预热迭代期间都存在IteratorAtomicLong(直到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)将创建多少其他类