学习笔记@Effective Java
文章内容来源于Joshua Bloch - Effective Java (3rd) - 2018.chm一书
第二章
创建和注销对象
Item 7消除过时的对象引用
如果你从一个手动的内存管理语言(比如C或C++)切换到一个垃圾回收的语言,比如java,你的程序员的工作就会变得更容易,因为你的对象在你完成这些任务时会自动回收。
它很容易给人留下这样的印象:你不必考虑内存管理,但这并不正确
// Can you spot the "memory leak"?
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错误,但是比较少见。
那么哪里内存泄漏呢?如果堆栈先增长后收缩,则从堆栈中弹出的对象将不会被垃圾收集,即使使用堆栈的程序不再引用它们。这就是由于过时引用
在这种情况下,任何引用元素数组的“活动部分”之外都是过时的。活动部分由index 小于size的元素组成
解决这类问题的方法很简单:一旦引用过时就清空。上述例子中,只要对象从stack被pop off 就过时了
正确的pop方法:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
消除过时引用的另一个好处是,如果它们随后被错误地取消引用,程序将立即失败并出现NullPointerException,而不是悄悄地做错误的事情。尽快检测编程错误总是有益的。
当程序员第一次掉这个坑时,他们可能全部清空来过度补偿。这既不必要,也不可取。
消除对象引用应该是例外,而不是常规。
消除过时引用的最佳方法是让包含引用的变量超出范围。如果您在尽可能窄的范围内定义每个变量,则会自然发生这种情况。
一般来说,每当一个类管理它自己的内存时,程序员应该警惕内存泄漏。每当释放元素时,元素中包含的所有对象引用都应为空。
另一个常见的内存泄漏源是缓存。
如果你巧合的实现了一个缓存,只要缓存外有对其键的引用,条目就与之相关,请将缓存表示为WeakHashMap;条目将在过时后自动删除。
记住WeakHashMap是好用的,只有当所需的缓存项生存期是由对键的外部引用而不是值确定时
更常见的情况是,缓存项的有效生存期定义得不太明确,随着时间的推移,缓存项的价值越来越低。在这些情况下,应该偶尔清除缓存中已停用的条目。这可以由后台线程(可能是ScheduledThreadPoolExecutor)完成,也可以作为向缓存中添加新项的副作用。LinkedHashMap类通过其removeEldestEntry方法简化了后一种方法。对于更复杂的缓存,您可能需要直接使用java.lang.ref。
第三种常见的内存泄漏源是侦听器和其他回调。如果您实现了一个API,其中客户机注册回调,但没有显式取消注册它们,除非您采取一些措施,否则它们将累积。确保回调被及时垃圾收集的一种方法是只存储对它们的弱引用,例如,只将它们存储为WeakHashMap中的键。