强制进行一次垃圾收集,并且对此时的堆做一份对象快照。
进行任何可以产生无意地保留的对象的操作。
再强制进行一次垃圾收集,然后对系统堆中的对象做第二次对象快照。
比较两次快照,看看哪些对象的被引用数量比第一次快照时增加了。因为您在快照之前强制进行了垃圾收集,那么剩下的对象都应该是被应用程序所引用的对象,并且通过比较两次快照我们可以准确地找出那些被程序保留的、新产生的对象。
根据您对应用程序本身的理解,并且根据对两次快照的比较,判断出哪些对象是被无意保留的。
跟踪这些对象的引用链,找出究竟是哪些对象在引用这些无意地保留的对象,直到您找到了那个根对象,它就是产生问题的根源。
显式地赋空(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. 仍在静态作用域中的对象

