第6条:消除过期的对象引用
Java的垃圾回收机制并不代表我们不需要考虑内存管理的问题。
考虑:
public class Stack { pprivate Object[] elements; private int size = 0; private static final int DEFAULT_INITAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if(size == 0) { throw new EmptyStackException(); } return elements[--size]; } private void ensureCapacity() { if(elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } }
这是自己编写的一个栈。
这段程序没有任何明显的错误,但这个程序中隐藏着一个问题,内存泄漏。
如果一个栈先是增长,然后收缩,那么从栈中弹出来的对象不会被当作垃圾回收,这是因为栈内部仍然维护着这些过期对象的引用,所谓过期引用是指elements中下标大于等于size的那些元素(由于栈会增长收缩,所以这是完全有可能的),如果一个对象被无意识地(即我们不希望地)保留下来,那么这个引用所引用的其他对象也不会被垃圾回收机制回收,因为仍然存在着引用,GC认为这些对象仍然是会被使用的。随着程序的运行时间增长,过期的对象引用占用的内存会越来越大,导致程序性能下降。
这类问题的解决方法是:一旦对象引用已经过期,只需清空这些引用即可。
public Object pop() { if(size == 0) { throw new EmptyStackException(); } Object result = elements[--size]; elements[size] = null;//显式地清空引用 return result; }
清空对象引用应该是一种例外,而不是一种规范行为,不要求也不建议程序员对于每个对象引用,一旦程序不再用,就把它清空,这通常会把代码弄的很混乱。
消除过期引用的最好办法是在最紧凑的作用域范围内定义每一个变量,当作用域被执行完,GC会自动把过期作用域的变量回收掉。
什么时候应该自己清空引用?
一般而言,只要是自己管理内存,就应该警惕内存泄漏问题。假如你开辟了一段内存空间,并一直持有这段空间的引用,就有责任管理它,因为GC无法自动完成对你承诺管理的内存的回收,除非你告诉它(显式地清空引用)。
在JDK中,已经有现成的Stack类供我们使用,来看看它是怎么实现pop的:
public synchronized E pop() { E obj; int len = size(); obj = peek();//这个方法将栈顶的元素取出来,但并不会把栈顶元素弹出 removeElementAt(len - 1);//这是Stack父类的方法,将栈顶元素弹出 return obj; } public synchronized void removeElementAt(int index) { modCount++; if (index >= elementCount) { throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount); } else if (index < 0) { throw new ArrayIndexOutOfBoundsException(index); } int j = elementCount - index - 1; if (j > 0) { System.arraycopy(elementData, index + 1, elementData, index, j); } elementCount--; elementData[elementCount] = null; /* to let gc do its work *//可以看到,jdk的实现就是显式地把引用清空,以此告诉GC将过期引用回收 }
内存泄泄漏通常不会表现成明显的失败,可以在系统中存在很多年,只有通过检查代码,或借助Heap剖析工具才能发现内存泄漏问题。所以要尽量在内存泄漏发生之前就知道如何预测此类问题。