我们使用Java的方法非常频繁地被调用,例如每秒10'000到20'000次(数据流系统)。让我们回顾一下以下简单的test方法(有意简化并且不产生实际价值):
public void test() {
Stream.of(1, 2, 3, 4, 5)
.map(i -> i * i)
.filter(new SuperPredicate())
.sorted(Comparator.comparing(i -> -i + 1, Comparator.nullsFirst(Comparator.naturalOrder())))
.forEach(System.out::println);
}
class SuperPredicate implements Predicate<Integer> {
public SuperPredicate() {
System.out.println("SuperPredicate constructor");
}
@Override
public boolean test(Integer i) {
return i % 3 != 0;
}
}在每次调用test方法时,都会创建新的函数接口实例(在我们的示例中,是SuperPredicate和Comparator.nullsFirst())。因此,对于频繁的方法调用,将创建数千个多余的对象。我理解在Java中创建对象只需几纳秒,但是如果我们谈论的是高负载,它也可能增加GC的负载,从而影响性能。
正如我所看到的,我们可以将这样的函数接口的创建移到同一个类中的private static final变量中,因为它们是无状态的,所以它会稍微减少系统上的负载。这是一种微观优化。我们需要这样做吗?Java编译器/ JIT编译器是否以某种方式优化了这种情况?或者编译器有一些选项/优化标志来改善这种情况?
发布于 2020-06-03 13:22:24
当对象不依赖于周围上下文的变量时,您只能将对象存储在static final字段中以供重用,更不用说状态的潜在变化了。
在这种情况下,根本没有理由创建像SuperPredicate这样的类。您可以简单地使用更高级的、更高级的i -> i % 3 != 0,并获得免费记住第一个创建实例的行为。正如Does a lambda expression create an object on the heap every time it's executed?中所解释的,在参考实现中,为非捕获lambda表达式创建的实例将被记住并重用。
也没有必要建立一个新的比较国。将潜在溢出搁置一边,使用函数i -> -i + 1只会因为否定而逆转顺序,而+1对顺序没有影响。因为表达式-i + 1的结果永远不可能是null,所以不需要Comparator.nullsFirst(Comparator.naturalOrder())。因此,您可以使用Comparator.reverseOrder()替换整个比较器,结果相同,但不包含任何对象实例化,因为reverseOrder()将返回共享的单例。
正如在What is the equivalent lambda expression for System.out::println中所解释的,方法引用System.out::println正在捕获System.out的当前值。因此,引用实现不会重用引用PrintStream实例的实例。如果我们将它更改为i -> System.out.println(i),它将是一个非捕获的lambda表达式,它将在每个函数计算中重新读取System.out。
所以当我们用
Stream.of(1, 2, 3, 4, 5)
.map(i -> i * i)
.filter(i -> i % 3 != 0)
.sorted(Comparator.reverseOrder())
.forEach(i -> System.out.println(i));与示例代码不同,我们得到相同的结果,但是保存四个对象实例化,用于谓词、使用者、nullsFirst(…)比较器和comparing(…)比较器。
为了估计这种保存的影响,Stream.of(…)是一个varargs方法,因此将为参数创建一个临时数组,然后返回一个表示流管道的对象。每个中间操作创建另一个临时对象,表示流管道的已更改状态。在内部,将使用Spliterator实现实例。这使得总共有6个临时对象,只用于描述操作。
当终端操作开始时,将创建一个表示该操作的新对象。每个中间操作将由具有对下一个使用者的引用的Consumer实现表示,因此组合的使用者可以传递给Spliterator的forEachRemaining方法。因为sorted是一个有状态的操作,它将将所有元素存储到一个中间ArrayList中(首先生成两个对象),在将它们传递给下一个使用者之前对其进行排序。
这使得总共有12个对象,作为流管道的固定开销。操作System.out.println(i)将每个Integer对象转换为由两个对象组成的String对象,因为每个String对象都是数组对象的包装器。这为这个特定的示例提供了10个额外的对象,但更重要的是,每个元素有两个对象,因此对于更大的dataset使用相同的流管道将增加在操作期间创建的对象的数量。
我认为,在幕后和之前创建的临时对象的实际数量,使得保存四个对象变得无关紧要。如果分配和垃圾收集性能与您的操作相关,您通常必须关注每个元素的成本,而不是流管道的固定成本。
https://stackoverflow.com/questions/62106608
复制相似问题