类似如下的
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];
}
/**
* 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);
}
}
这个程序就是一个简单的栈的实现,但是有一个隐晦的内存泄露,也就是pop的时候,我们没有把 ·elementssize == null·。
当我们 push了,再pop的时候,被丢弃的位置,还留有元素的引用,栈会一直保存着这些过期引用,所以就内存泄露了。
解决办法就是,设置为 null,这还有个好处,如果它们最后被错误的引用,还会爆出 NullPointerException 异常。
如果一个类自己管理内存的时候,程序员就应该警惕内存泄露问题。
就像是上面的Stack一样,它有两部分组成,一个是有元素引用的,一个是空闲部分。虽然我们知道,但是对于程序来说,他不知道,所以我们必须自己手动去处理这些空闲部分的引用。
另一个常见的内存泄露是缓存。
我们很容易忘记缓存的存在,不知不觉间就爆内存了。
对此,我们会有几种解决方案。
WeakHashMap,它的key 是弱引用。当外部决定了key的引用,而不是由key本身决定,那么我们就可以用weakHashMap。
举个例子:
WeakHashMap<UniqueImageName, BigImage> cache = new WeakHashMap<>();
UniqueImageName name = new UniqueImageName("avatar.png");
BigImage image = new BigImage("avatar.png");
cache.put(name, image);
-
这里的key,是由强引用 name引用的,所以entry 不会被回收。
-
当我们把 name = null 之后,这个entry就会被回收。
那什么情况下,weakHashMap没有用呢?就是key 被强引用了,例如,你的key 是String字符串,String字符串,我们都知道,会以字面量的形式一直存在于内存中,所以一直都会有强引用,那么这个entry 就一直都会被引用,不会被垃圾回收。
更为常见的情况是,缓存项有用的声明周期不太明确,随着时间的推移有一些项会变得没什么价值。在这种情况下,可以偶尔清理掉已经废弃的项。
- 可以通过一个后天线程(例如 ScheduledThreadPoolExecutor)或者新push 的时候,清理掉一些缓存
- 又或者像LinkedHashMap 一样 使用removeEldestEntry去实现
- 更复杂的缓存,可以直接使用 java.lang.ref
LinkedHashMap 中的 removeEldestEntry,这个方法如下
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
我们这里用他的子类,LRUCache 来说明这个例子。
如果我们要使用 removeEldestEntry这个方法的话,子类必须重写这个方法,返回值为true才会删除
ublic class LRUCache<K, V> extends LinkedHashMap<K, V> {
private static final long serialVersionUID = 1L;
protected int maxElements;
public LRUCache(int maxSize) {
super(maxSize, 0.75F, true);
this.maxElements = maxSize;
}
@Override
protected boolean removeEldestEntry(Entry<K, V> eldest) {
return size() > this.maxElements;
}
}
这里的 LRUCache就实现了这个方法。我们不用管他的实现是什么意思,我们只要知道,他最后会返回true。
LRUCache ,就是把访问率低的缓存,给丢掉。每次get 的时候,都会把get上的 元素移动到链表的尾部。当我们的缓存满的时候,就会丢失开头的那个元素。 这就是LRU Least Recently Used
第三个常见的内存泄露的来源是监听器和其他回调。
public class MainActivity extends Activity {
private Button button;
@Override
protected void onCreate() {
button = findViewById(R.id.btn);
// 注册回调,this 被传给了 button
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick() {
// 内部类隐式持有外部类 MainActivity 的引用
doSomething();
}
});
}
}
如上图,正常来说,MainActivity 被关闭了,整个类都会被回收,但是回调里边有其他引用,那么整MainActivity都不会被回收 -》 内存泄露
所以每次打开MainActivity都会注册一个新的监视器,旧的永远都不会被移除,越积越多。
如何避免呢?
显式的撤销注册
button.setOnClickListener(null);
使用弱引用 WeakReference,将这些回调放到 WeakReference里边