我记得在有一次面试中,面试官问我本人实现的一个栈中会不会有内存泄露的问题,我致力搜寻可能的问题,就是感触不到可能呈现的问题。过后突然意识到,内存泄露这个问题始终被我疏忽,因为用的是 java/C#, 这些语言中都有内存主动回收的机制,我忽然发现自己对这个问题居然无所不知。面试中的栈就是上面这个:
// 你能查看出 "内存泄露" 吗?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);
}
}
这段程序不论你怎么测试都是没有问题的,然而他的确可能引起“内存泄露”。定位到 pop() 函数,在 return 语句中,当咱们弹出一个元素时,只是简略的让栈顶指针(size)-1。逻辑上,栈中的这个元素曾经弹出,曾经没有用了。然而事实上,被弹出的元素仍然存在于 elements 数组中,它仍然被 elements 数组所援用,GC 是无奈回收被援用着的对象的。兴许你冀望等这整个栈失去援用(将被 GC 回收时),栈内的 elements 数组一起被 GC 回收。然而理论的应用过程中,又有谁可能预料到这个栈会存活多长时间。为了保险起见,咱们须要在弹出一个元素的时候,就让这个元素失去援用,便于 GC 回收。咱们只须要让 Pop() 函数弹出时,同时解除对弹出元素的援用即可。
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 打消过期的援用
return result;
}
从下面的例子中,咱们能够发现当类中持有过期的元素的援用时,就有可能造成内存泄露问题。而且通常这种内存泄露问题都是咱们有意识造成的,下面的栈中,逻辑上咱们认为弹出的元素就应该被 GC 回收掉,但事实上 GC 没有方法回收,因而 elements 数组仍然持有它。这种问题很荫蔽,通常只有类本人治理内存(如类中有一个 Array 或 List 型的构造),那么咱们就应该警觉内存泄露的问题。
内存泄露起源及解决
内存泄露可能来源于缓存。咱们为了让下次的程序的处理速度更快,经常须要将一些信息缓存在内存中,然而这些过期的缓存又很容易被忘记,从而使得它不再有用之后很长一段时间内依然留在缓存中。例如像一个要显示图片墙的程序,咱们须要缓存图片和相干的信息,为了不便 GC 回收过期的缓存,咱们能够应用 WeakHashMap 来实现缓存,当界面显示图片的时候,界面持有相干图片的援用,这些援用同时也存在于 WeakHashMap 中。而其余不被界面持有的过期缓存,则 WeakHashMap 会主动将这些剔除。总的说来,只有在缓存之外存在对某个项的键的援用,该项就有意义,那么就能够用 WeakHashMap 代表缓存;当缓存中的项过期之后,它们就主动被删除。记住只有当所有的缓存项的生命周期是由该键的内部援用而不是由值决定时,WeakHashMap 才有用途。更为常见的情景则是,"缓存项的生命周期是否有意义" 并不是非常容易确定,随着时间推移,其中的项会变得越来越没有价值。在这种状况下,缓存应该是不是地革除掉没有的项。这项革除工作能够由一个后盾线程(可能是 Timer 或者 ScheduledThreadPoolExecutor)来实现,或者也能够在给缓存增加新条目标时候顺便进行清理。LinkedHashMap 类利用它的 removeEldestEntry 办法能够很容易地实现后一种计划。对于更加简单的缓存,必须间接应用 java.lang.ref.
内存泄露的第三个常见起源是监听器和其余回调。如果你实现了一个 API,客户端在这个 API 中注册回调,却没有显式地勾销注册,那么除非你采取某些动作,否则他们就会积累。确保回调立刻被当成垃圾回收的最佳办法是只保留它们的弱援用(weak reference),例如,只将它们保留成 WeakHashMap 中的键。
局部文字间接截取自《Effective Java》