java中可能的内存泄露
引子
一般情况下Java会自动回收内存,但是有些情况下还是会有可能发生内存泄露,比如下面这段代码
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];
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
这段代码看起来功能上没有什么问题,基本能通过每一项测试,然而却隐藏着一个问题,那就是可能会导致内存泄露。
哪里发生了内存泄露呢?
这个栈空间如果先增长,又缩减了。这段代码在出栈的时候,做了把size-1和对应位置元素返回,然后这时size-1位置(第size个元素)的元素引用仍然是存在的,导致在gc的时候并不会被回收。这是因为在栈内部维护者这些对象的过期引用,所谓的过期引用,是指永远不会再被解除的引用。其实在这里对象引用下标大于等于size的都是过期引用。
修复方法
这类问题的修复方法很简单,如下代码
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
总结
一般来说,只要类是自己管理内存,开发者就应该警惕内存泄露问题。
内存泄露的另一个常见来源是缓存(主要是容器缓存)。放进缓存后很容易被遗忘,使得不再使用之后很长一段时间仍然在缓存中。这类解决方法一般可以参考下面几种:
-
使用WeakhashMap,不过这个要记住 当所要的缓存项的声明周期是由该键的外部引用而不是值决定时 WeakHashMap才有用
-
用一个后台线程来完成清除操作
-
给缓存添加新条目时顺便清理
内存泄露第三种常见来源是监听器和其他回调。比如你实现了个API,客户端在这个API中注册了回调,然而没有显式的取消注册,如果没有采取某些动作,它们就会不断的累计起来。这种解决方式可以通过只保存它们的弱引用,例如,只将它们保存成WeakHashMap的中键。