Java 优化是否需要提取到 static final?

Is extracting to static final necessary for Java optimization?

考虑这个方法:

private void iterate(List<Worker> workers) {
    SortedSet<Worker> set = new TreeSet<>(new Comparator<Worker>() {
        @Override
        public int compare(Worker w0, Worker w1) {
            return Double.compare(w0.average, w1.average);
        }
     });

     // ... 
}

如您所见,该集合正在创建一个带有自定义比较器的新 TreeSet

我想知道从 performance/memory/garbage collection/whatever 的角度来看是否有任何区别,如果我这样做而不是污染外部 space:

static final Comparator<Worker> COMPARATOR = new Comparator<Worker>() {
    @Override
    public int compare(Worker w0, Worker w1) {
        return Double.compare(w0.average, w1.average);
    }
};

private void iterate(List<Worker> workers) {
    SortedSet<Worker> set = new TreeSet<>(COMPARATOR);
    // ... 
}

我问的原因是,我觉得 编译器 应该已经解决了这个问题并为我优化了它,所以我不应该提取它,对吧?

同样的事情也适用于在方法中声明的字符串或其他临时的、不可变的 对象。

提取一个 final 变量会有什么不同吗?

注意:我知道这对性能提升的影响很小。问题是是否存在 任何 差异,无论如何可以忽略不计。

一个很大的区别是由 "hidden" 暗示匿名 classes 持有对包含 class 的隐式引用引起的,因此如果您将该 TreeSet 传递给另一个进程,一个对您的 class 实例的引用是由另一段代码通过匿名比较器通过 TreeSet 保存的,因此您的实例不会被垃圾收集。

这可能会导致内存泄漏。

但是选项 2 不会遇到这个问题。

否则就是作风问题了


在java8中,可以改用lambda表达式,两全其美。

会有不同是的。

  • CPU 影响:分配给 static 减少了每次分配新比较器所需的工作量
  • GC效果:每次分配一个新对象,然后立即丢弃,不会有young GC成本;然而,将它分配给一个变量会增加 GC 时间(非常非常小),因为它是一组额外的需要遍历的引用。死对象不花钱,活对象需要。
  • 内存效应:将比较器分配给一个常量将减少每次调用该方法所需的内存量,以换取较低的常量开销,该开销将移至终身 GC space。
  • 引用逃逸的风险:内部 classes 包含指向构造它的 class 的指针。如果内部 class (比较器)曾经从创建它的方法中返回,那么对父对象的强引用可能会逃逸并阻止父对象的 GC。纯粹是一个可以潜入代码的陷阱,在这个例子中不是问题。

Hotspot 非常擅长内联,但不太可能认识到比较器可以在堆上分配或移动到常量。但这将取决于 TreeSet 的内容。如果 TreeSet 的实现非常简单(而且很小),那么它可以内联,但我们都知道事实并非如此。 TreeSet 也被编码为通用的,如果它只与一种类型的对象(Worker)一起使用,那么 JVM 可以应用一些优化,但是我们应该假设 TreeSet 也会被其他类型使用,因此 TreeSet 不会能够对传递给它的比较器做出任何假设。

因此,两个版本之间的区别主要在于对象分配。使用 final 关键字不太可能提高性能,因为 Hotspot 大多数情况下都会忽略 final 关键字。

Java 8 在使用 lambda 时有一个非常有趣的行为。考虑示例的以下变体:

import java.util.*;

public class T {
    public void iterate(List<String> workers) {
        SortedSet<Double> set = new TreeSet<>( Double::compare );
    }
}

运行'javap -c T.class',你会看到如下jvm代码:

  public void iterate(java.util.List<java.lang.String>);
    Code:
       0: new           #2                  // class java/util/TreeSet
       3: dup
       4: invokedynamic #3,  0              // InvokeDynamic #0:compare:()Ljava/util/Comparator;
       9: invokespecial #4                  // Method java/util/TreeSet."<init>":(Ljava/util/Comparator;)V
      12: astore_2
      13: return

这里要注意的很酷的事情是 lambda 没有对象构造。 invokedynamic 在第一次被调用时会有更高的成本,然后它被有效地缓存。