对象引用是怎样严重影响垃圾收集器的
作者: 来源: 添加时间:2006-5-24 13:02:22显式地赋空(nulling)变量
一谈到垃圾收集这个主题,总会涉及到这样一个吸引人的讨论,即显式地赋空变量是否有助于程序的性能。赋空变量是指简单地将 null 值显式地赋值给这个变量,相对于让该变量的引用失去其作用域。
清单 1. 局部作用域
public static String scopingExample(String string) {
StringBuffer sb = new StringBuffer();
sb.append("hello ").append(string);
sb.append(", nice to see you!");
return sb.toString();
}
当该方法执行时,运行时栈保留了一个对 StringBuffer 对象的引用,这个对象是在程序的第一行产生的。在这个方法的整个执行期间,栈保存的这个对象引用将会防止该对象被当作垃圾。当这个方法执行完毕,变量 sb 也就失去了它的作用域,相应地运行时栈就会删除对该 StringBuffer 对象的引用。于是不再有对该 StringBuffer 对象的引用,现在它就可以被当作垃圾收集了。栈删除引用的操作就等于在该方法结束时将 null 值赋给变量 sb。
错误的作用域
既然 Java 虚拟机可以执行等价于赋空的操作,那么显式地赋空变量还有什么用呢?对于在正确的作用域中的变量来说,显式地赋空变量的确没用。但是让我们来看看另外一个版本的 scopingExample 方法,这一次我们将把变量 sb 放在一个错误的作用域中。
清单 2. 静态作用域
static StringBuffer sb = new StringBuffer();
public static String scopingExample(String string) {
sb = new StringBuffer();
sb.append("hello ").append(string);
sb.append(", nice to see you!");
return sb.toString();
}
现在 sb 是一个静态变量,所以只要它所在的类还装载在 Java 虚拟机中,它也将一直存在。该方法执行一次,一个新的 StringBuffer 将被创建并且被 sb 变量引用。在这种情况下,sb 变量以前引用的 StringBuffer 对象将会死亡,成为垃圾收集的对象。也就是说,这个死亡的 StringBuffer 对象被程序保留的时间比它实际需要保留的时间长得多――如果再也没有对该 scopingExample 方法的调用,它将会永远保留下去。
一个有问题的例子
即使如此,显式地赋空变量能够提高性能吗?我们会发现我们很难相信一个对象会或多或少对程序的性能产生很大影响,直到我看到了一个在 Java Games 的 Sun 工程师给出的一个例子,这个例子包含了一个不幸的大型对象。
清单 3. 仍在静态作用域中的对象
private static Object bigObject;
public static void test(int size) {
long startTime = System.currentTimeMillis();
long numObjects = 0;
while (true) {
//bigObject = null; //explicit nulling
//SizableObject could simply be a large array, e.g. byte[]
//In the JavaGaming discussion it was a BufferedImage
bigObject = new SizableObject(size);
long endTime = System.currentTimeMillis();
++numObjects;
// We print stats for every two seconds
if (endTime - startTime >= 2000) {
System.out.println("Objects created per 2 seconds = " + numObjects);
startTime = endTime;
numObjects = 0;
}
}
}
这个例子有个简单的循环,创建一个大型对象并且将它赋给同一个变量,每隔两秒钟报告一次所创建的对象个数。现在的 Java 虚拟机采用 generational 垃圾收集机制,新的对象创建之后放在一个内存空间(取名 Eden)内,然后将那些在第一次垃圾收集以后仍然保留的对象转移到另外一个内存空间。在 Eden,即创建新对象时所在的新一代空间中,收集对象要比在“老一代”空间中快得多。但是如果 Eden 空间已经满了,没有空间可供分配,那么就必须把 Eden 中的对象转移到老一代空间中,腾出空间来给新创建的对象。如果没有显式地赋空变量,而且所创建的对象足够大,那么 Eden 就会填满,并且垃圾收集器就不能收集当前所引用的这个大型对象。所产生的后果是,这个大型对象被转移到“老一代空间”,并且要花更多的时间来收集它。
通过显式地赋空变量,Eden 就能在新对象创建之前获得自由空间,这样垃圾收集就会更快。实际上,在显式赋空的情况下,该循环在两秒钟内创建的对象个数是没有显式赋空时的5倍――但是仅当您选择创建的对象要足够大而可以填满 Eden 时才是如此, 在 Windows 环境、Java虚拟机 1.4 的默认配置下大概需要 500KB。那就是一行赋空操作产生的 5 倍的性能差距。但是请注意这个性能差别产生的原因是变量的作用域不正确,这正是赋空操作发挥作用的地方,并且是因为所创建的对象非常大。
最佳实践
这是一个有趣的例子,但是值得强调的是,最佳实践是正确地设置变量的作用域,而不要显式地赋空它们。虽然显式赋空变量一般应该没有影响,但总有一些反面的例子证明这样做会对性能产生巨大的负面影响。例如,迭代地或者递归地赋空集合内的元素使得这些集合中的对象能够满足垃圾收集的条件,实际上是增加了系统的开销而不是帮助垃圾收集。请记住这是个有意弄错作用域的例子,其实质是一个无意识的对象保留的例子。
第 2 页,共 2 页 [1] [2]
站内搜索