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];
}
/**
* 扩容
*/
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
public class Demo {
public static void main(String[] args) {
Stack stack =new Stack();
stack.push(Integer.valueOf(1));
stack.push(Integer.valueOf(2));
stack.push(Integer.valueOf(3));
stack.push(Integer.valueOf(4));
stack.push(Integer.valueOf(5));
Integer numInteger = (Integer) stack.pop();
System.out.println(numInteger);
}
}
执行上述代码,从结果上面看好像也没啥问题。但其实先入栈5个元素,然后再出栈1个元素,那么此时出栈的这个元素是不会被销毁,因为elements中仍然持有指向该出栈对象的引用,会出现内存泄漏。因此我们可对上述代码进行如下优化:
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();
}
Object result = elements[--size];
elements[size]=null;
return result;
}
/**
* 扩容
*/
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
2、内存泄漏的另一个常见来源是缓存
对象引用存放在缓存中,当对象不再被使用时,然后你忘记将该对象引用从缓存中清理掉。于是该对象引用会一直保存在缓存中,而你在逻辑上已经没有使用该对象,但是该对象不会被GC回收,因为仍然有引用指向它。
解决办法:关键是要知道什么时候,缓存中的对象引用不再有用。在这个时候,就可以清理掉缓存中的对象引用。
一种例子是,如果实现的缓存是这样的:只要在缓存之外存在对某个项的键的引用,该项就有意义;如果没有存在对某个项的键的引用,该项就没有意义,那么我们可以使用WeakHashMap来代表缓存。
import java.util.Map;
import java.util.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);
}
}
WeakHashMap是一种弱引用的map,底层数据结构为数组+链表,内部的key存储为弱引用(除了自身有对key的引用,没有其它引用,也就是弱引用),在GC时如果key不存在强引用的情况下会被回收掉,而对于value的回收会在下一次操作map时回收掉,所以WeakHashMap适合缓存处理。
3、内存泄漏的第三个常见来源是监听器
在java编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXxxListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
内存泄漏通常不会表现的很明显,可以在系统中存在很多年,只有通过检查代码,或借助Heap剖析工具才能发现内存泄漏问题,所以要尽量在内存泄漏发生之前就知道如何预测此类问题。