当你从内存需要管理的语言(C
、C++
),跳转到基于GC
管理内存的语言时,你会发现要简单很多。因为GC
会自动回收不可用对象,它释放了你的工作。
但是GC
管理内存一定可靠吗?答案是未必。如:
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);
}
}
以上是一个简单的Stack
模拟。如果你测试,你会发现任何操作都是可以的。但是却有个不足:当取出栈顶数据的时候,栈顶数据未清空,使得GC
不能够回收栈顶空间。如此反复插入、取数据,最终可能导致OutOfMemoryError
。
如何修改呢?
很简单,将栈顶元素置空即可。即:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
//消除过期对象引用
elements[size] = null;
return result;
}
那是不是有点疑惑?以后我们在开发中是不是都需要将无需使用的对象置空?答案是否。置空对象应该是例外情况,而不能作为规范,否则GC
就是多余的了。
其实,当类需要自己管理本身的内存时,才需要消除过期对象的引用。开发者应该警惕内存泄漏问题。通常这样的类常常与数组相关联,开始时数组中都存储了数据,之后取出(不可用),但是GC
并不明白此时的取出操作,因为数据还是存储在内存中的,只有开发者知道此时的取出就是使数据不可用,所以开发者需要给GC
提示。
内存泄漏的另一常见来源是缓存。因为一旦你将数据存储在缓存中,而没有及时的进行清理,那就相当于内存泄漏。
监听器和回调也是导致内存泄漏的原因。如果有个API
,客户端在向其注入回调,但没有显示的取消注册,那除非你采用了某些动作取消,否则会一直积聚直至内存溢出为止。
在基于GC
的语言中,内存泄漏是很难被发觉的。平时只能依靠仔细检查代码,或借助于Heap Profiler
工具进行检查。