effective-java 第7条 消除过期的对象引用

类似如下的

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里边