消除过时的对象引用-第二章创建和销毁对象-Effective Java学习笔记06

文章内容来源于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中的键。

猜你喜欢

转载自blog.csdn.net/weixin_43596589/article/details/112787497