大量new临时对象可以是一种优化手段
有C++经验的人往往知道,频繁在堆上分配对象对性能伤害很大。比如这样的代码就不是好代码:
void foo(int[] a) {
WrappedArray* wrappedArray = new WrappedArray(a);
wrappedArray->bar();
delete wrappedArray;
}
然而在JVM(指HotSpot虚拟机)上完全不是这样的。尽管JVM不提供在栈上创建对象的功能,但JVM会自动通过逃逸分析找出生命周期在函数范围内的临时对象,然后运行时优化这个函数,把对象创建放到栈上。逃逸分析在JRE7里面已经默认开启了,对临时对象多的代码效果很大。
Scala的隐式类型转换特性很鼓励创建临时对象,标准库中有大量的隐式转换,会生成许多临时对象(如 WrappedArray
、 ArrayOps
、 StringAdd
、 WithFilter
等)。我读标准库的源码时甚至感到他们已经把创建临时对象当成一种优化手段了。
比如C++常见的pimpl模式往往会让用户代码持有一个包装对象的指针,包装对象的内部有一个底层实现对象的指针。这种做法在Scala里面就完全不被主张,而更鼓励长期持有底层对象,而在需要用时再临时包装成对外接口。
这是因为,pimpl模式比直接持有底层对象多保存一个包装对象。如果在容器中保存大量包装对象时,额外的内存开销就会多起来,而每次函数调用也都会多一次间接寻址的开销。另一方面,只持有底层对象的话,虽然每次使用都会生成临时对象,但是这些临时对象经过JVM的逃逸分析、标量替换和内联优化后是零开销的。
但临时对象的开销取决于JVM的优化,然而JVM要在运行时优化,而且只有频繁运行的代码才会被优化。所以如果一个程序创建很多临时对象,那么刚启动时它的性能会很差,而运行一段时间后会慢慢变好。这也是为什么我们经常会感觉Java程序启动慢的原因之一。