类自己管理内存
一般而言,只要是类自己管理内存,我们就应该警惕内存泄漏问题。比如下面的例子:
public class Stack {
private Object[] elements;
private int size;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
elements = new Objects[DEFAULT_INITIAL_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);
}
}
}
上面的示例代码是一个简单的栈实现,并无明显的错误,无论怎样测试,它都能成功通过每一项测试,但是这里隐藏着一个问题:如果我们先进行入栈操作,然后在再进行出栈操作,从栈中弹出来的对象不会被当做垃圾回收。比如一个栈先进行入栈操作,至size的值为100,然后进行出栈操作至size的值为50,此时elements数组的活动部分为50,而另一部分已经出栈的50歌对象则为过期引用,且不会被回收,随着时间的推移,就有可能对性能造成潜在的重大影响。
这类问题的修复方法也很简单,即一旦对象引用过期,立即解除引用,本例中修正为:
public Object pop(){
if (size == 0){
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
缓存
另一个容易造成内存泄漏的来源是缓存,假设我们现在使用LinkedHashMap构建一个简单缓存,以维护要缓存的对象:
public class Cache extends LinkedHashMap {
private static final Cache INSTANCE = new Cache();
private Cache(){}
public static Cache getInstance(){
return INSTANCE;
}
}
一旦我们把对象放入缓存,之后就很容易将他遗忘,随着时间的推移会变得越来越没有价值,这种情况下,我们应该将其清除。清除缓存可以由一个后台线程完成,或者在新加缓存数据时完成。示例代码中有则可以很容易实现第二种方案,LinkedHashMap中新加数据时会调用afterNodeInsertion(boolean)
对最老的数据进行处理,是否清理则由removeEldestEntry(Map.Entry)
的返回值决定,我们仅需覆盖该方法即可。
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
监听器和其他回调
除了上面提到的内容,其他常见来源就是监听器和回调,如果注册了监听或回调,却没有显示的取消,它们就会不断累积,应当显示地取消注册。
总结:要及时消除过期的对象引用。