《Effective Java 第三版》笔记之七 消除过期的对象引用
Item 7: Eliminate obsolete object references
如果你从一个需要手动管理内存的语言如C/C++等,切换到有垃圾回收的语言,如Java,你的工作将感觉容易得多,因为当你不需要使用某个对象的时候,它们会自动回收这个对象。这很自然地导致你认为不需要去管理内存,但实际上这是不对的。
考虑如下一个Stack的实现:
// 这种实现包含了内存泄漏
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);
}
}
上述程序并没有明显的问题,你可以疯狂的测试它,它都能很出色的完成任务,但是它却有一个潜在的问题。笼统地讲,程序有“内存泄漏(memory leak)”的风险,由于增加了垃圾回收器的工作或者增加了内存占用导致了性能的下降。在极端的情况下,这个程序可能会导致OutOfMemoryError(内存溢出)。当然,这种情况很少。
那么内存泄漏出现在哪呢?如果一个stack先增长然后再收缩,那么从stack中踢出去的对象并不会被垃圾回收。因为stack本身管理了这些过时引用的对象。一个过时引用就是那些从来不会被再一次引用的对象。在这里,任何在数组外的对象都是过时的。活跃的部分只有那些索引低于size的对象。
有垃圾回收器的编程语言中,内存泄漏的危害通常都不是显性的。如果某个引用对象被无意中保留了下来,那么这个对象不会被垃圾回收器识别,同时所有引用这个对象的对象也不会被回收。即便只有很少的对象被无意保留下来,也可能导致大量的变量被排除在垃圾回收之外,进而影响程序的性能。
修复这种问题也很简单:当某个对象过时了就让它变成null。在这个例子中,一旦某个都像被拉出stack,这个对象就过时了,因此上述例子中的pop方法应该改成如下:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
将上述过时引用变为空值还有个额外的好处就是如果这些对象被错误的引用会报空指针错误(NullPointerException),而不是继续错误下去。尽早的诊断出程序的错误是一件非常有意义的事情。
当程序员首次遇到这个问题的时候,他们也可能习惯于在程序完成了之后立即将所有的对象变成空值。但是这既没必要,也不是我们所期望的,实际上它会使得代码很混乱。让对象引用变成空值应该是一种意外处理而不是正常处理方式。最佳的消除过时引用的处理方式应当是让包含过时引用的变量失效,如果你将每个对象都按照最窄范围原则定义,这就很自然的发生了。
那么,应该在什么时候将引用变成空值呢?Stack类的哪方便容易造成内存泄漏呢?简单来说,就是它自己管理自己的内存。即存储池是由elemens数组中的元素组成(存放的是引用,不是对象本身)。在早期被定义的活跃部分的元素是有用的,而剩下的元素则是不重要的。而垃圾回收器却不知道,对于垃圾回收器来说,elemens数组中元素都是等价有效的。只有程序员才知道不活跃的那部分数组元素是不重要的。在这里,程序员通过将引用变成空值来通知垃圾回收器,这部分对象是不重要的。
一般来说,当一个程序自行管理自己的内存的时候,就需要注意内存泄漏。当某个元素自由之后(不被访问),任何这个元素包含的对象引用都要变成空值。
另一个内存泄漏的来源是缓存。一旦你将某个对象引用放入缓存,那就很容易忘记这个对象的存在,而这个对象即便变得无用之后也会在缓存中驻留很长时间。有几个方案可以解决这个问题。如果你正好想实现了一个缓存:只要在缓存之外存在对某个项(entry)的键(key)引用,那么这项就是明确有关联的,就可以用WeakHashMap来表示缓存;这些项在过期之后自动删除。记住,只有当缓存中某个项的生命周期是由外部引用到键(key)而不是值(value)决定时,WeakHashMap才有用。
更常见的情况是,缓存项有用的生命周期不太明确,随着时间的推移一些项变得越来越没有价值。在这种情况下,缓存应该偶尔清理掉已经废弃的项。这可以通过一个后台线程(也许是ScheduledThreadPoolExecutor)或将新的项添加到缓存时顺便清理。LinkedHashMap类使用它的removeEldestEntry方法实现了后一种方案。对于更复杂的缓存,可能直接需要使用java.lang.ref。
第三个常见的内存泄漏来源是监听器和其他回调。如果你实现了一个API,其客户端注册回调,但是没有显式地撤销注册回调,除非采取一些操作,否则它们将会累积。确保回调是垃圾收集的一种方法是只存储弱引用(weak references),例如,仅将它们保存在WeakHashMap的键(key)中。
因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。 通常仅在仔细的代码检查或借助堆分析器( heap profiler)的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的。
欢迎大家关注DataLearner官方微信,接受最新的AI技术推送
