【背景】java虽然有自己的垃圾回收机制,但是并没有那么的智能,对于被引用的对象,就算我们已经不在使用它了,但是java的回收机制是不会回收他们的,人们称之为“内存泄漏”。
【内存泄露场景】
1.只要类自己管理内存,就该警惕内存泄露问题。
如Stack类自己管理内存,在元素出栈,忘记设置为Null时,容易引起内存泄露。
import java.util.Arrays;
import java.util.EmptyStackException;
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);
}
}
}
先进行入栈5个元素,然后,再出栈5个元素。那么此时,会有5个元素不会被销毁,因为,elements中,持有指向5个对象的引用(保存了5个已经出栈的引用,只有再一次入栈5个引用)。出现内存泄露。进行优化如下:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
2.内存泄露的另一个常见来源是缓存。
对象引用存放在缓存中,当对象不再被使用时,然后,你忘记将该对象引用从缓存中清理掉。于是,该对象引用会一直保存在缓存中,而你在逻辑上已经没有使用该对象,但是该对象不会被GC回收,因为仍然有引用指向它。
解决办法:关键是,要知道什么时候,缓存中的对象引用不再有用,有意义。在这个时候,就可以清理掉缓存中的对象引用。
一种例子是,如果实现的缓存是这样的:只要在缓存之外存在对某个项的键的引用,该项就有意义;如果没有存在对某个项的键的引用,该项就没有意义。那么,可以使用WeakHashMap来代表缓存。
public class SocketManager {
private Map<Socket,User> m = new WeakHashMap<Socket,User>();
public void setUser(Socket s, User u) {
m.put(s, u);
}
public User getUser(Socket s) {
return m.get(s);
}
}
3.内存泄露的第三个常见来源是监听器和其他回调。
如果你实现了一个API,客户端在这个API中注册回调,却没有显式的取消注册,那么除非你采取某些动作,否则,它们会积聚。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用,例如,只将它们保存成WeakHashMap中的键。
【additional】内存泄泄漏通常不会表现成明显的失败,可以在系统中存在很多年,只有通过检查代码,或借助Heap剖析工具才能发现内存泄漏问题。所以要尽量在内存泄漏发生之前就知道如何预测此类问题。