移除过期的对象引用

这节讲的是关于内存泄露的问题。Java有三大主要的内存泄露来源:

1.开发者自己维护的内存

2.缓存

3.listener和callback

第一类:开发者自己维护的内存。这类问题通常是开发者从其它编程语言(例如c++)带来的坏习惯导致,比如下面这段代码:

// Can you spot the "memory leak"?
public class Stack {
	private Object[] elements;
	private int size = 0;
	private static final int DEFAULT_INITIAL_CAPACITY = 16;
	
	public Stack() {
		elements = new Object[DEFAULT_INITIAL_CAPACITY];
	}
	
	public void push(Object e) {
		ensureCapacity();
		elements[size++] = e;
	}
	
	public Object pop() {
		if (size == 0)
			throw new EmptyStackException();
		return elements[--size];
	}
	
	/**
	* Ensure space for at least one more element, roughly
	* doubling the capacity each time the array needs to grow.
	*/
	private void ensureCapacity() {
		if (elements.length == size)
			elements = Arrays.copyOf(elements, 2 * size + 1);
		}
}

这个栈的问题就在pop()方法:每弹出一个元素,栈指针下移一格,但这个指针是由开发者维护,jvm在技术层面对它一无所知。开发者在指针下移后知道,高于指针位置的元素不再有用,但jvm不知道这些。对于它来说,elements数组的容量没有变化,因而所有elements中的元素都仍有引用存在,不会被GC处理,最终已经弹出的元素所占用的内存不会被释放。解决方法是手动移除这些引用:

public Object pop() {
	if (size == 0)
		throw new EmptyStackException();
	Object result = elements[--size];
	elements[size] = null; // Eliminate obsolete reference
	return result;
}

这样处理的另一个好处是,一旦这段代码被错误调用,由于有元素被错误清除,会很快有NPE弹出,而不是使程序错误地执行下去。同时要注意,这种手动移除引用的方法不可滥用,只能作为处理这类特殊情况的办法。正常情况下,应该依赖变量的作用域来自动处理引用,即只在绝对必要的最小范围内定义变量。

第二类:缓存,即cache

比较简单的情况,是缓存元素的生命周期只依赖于缓存外对它的引用,一旦不再有外部引用,元素就可以销毁。这时只要用WeakHashMap来实现缓存即可。另一种情况是缓存元素有其自己的对象生命周期,这时就需要一个管理线程来定期清理缓存了,比较有用的工具是Timer,ScheduledThreadPoolExecutor,还有LinkedHashMap的removeEldestEntry方法。

第三类内存泄露来源是listener和各种callback

主要是确保deregister和register匹配,还有尽可能只存储callback的弱引用(weak reference),比如把它们作为key存储在WeakHashMap中。