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 在第一次被调用时会有更高的成本,然后它被有效地缓存。
考虑这个方法:
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 在第一次被调用时会有更高的成本,然后它被有效地缓存。