《Effective Java》学习笔记7 Eliminate obsolete object references

版权声明:欢迎转载,但麻烦注明出处 https://blog.csdn.net/q2878948/article/details/81327387

本栏是博主根据如题教材进行Java进阶时所记的笔记,包括对原著的概括、理解,教材代码的报错和运行情况。十分建议看过原著遇到费解地方再来参考或与博主讨论。致敬作者Joshua Bloch跟以杨春花为首的译者团队,以及为我提供可参考博文的博主们。

消除过期对象引用

相较C或者C艹这种需要手动回收资源的语音,Java的GC(Gabage Collector)帮我们偷了个大懒,对象用完之后会被GC自动回收。但这并不意味着不需要我们考虑内存管理的事情了。请看一个内存泄漏的例子,下面代码通过将栈内引用置空,防止内存泄漏,避免对性能产生影响。

public class MemoryLeakTest {
    private Object [] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public MemoryLeakTest(){
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object obj){
        ensureCapacity();
        elements[size++] = obj;;
    }
    @Deprecated
    public Object badPop(){
        if (size == 0){
            throw new EmptyStackException();
        }
        return elements[--size];
    }

    private void ensureCapacity(){
        if (elements.length == size){
            elements = Arrays.copyOf(elements , 2*size + 1);
        }
    }
}

这一段程序虽然没有明显错误,但是却隐藏着一个问题:如果这个栈先增长后收缩,那么从栈中弹出来的对象不会被当作辣鸡处理,就算使用栈的程序不再引用这些对象也是如此。

额,也顺便科普一下为啥内存泄漏。

内存泄漏

例如常见的Object obj = new Object(); obj就是一个强引用,当这个引用存在时JVM直到内存耗尽都不会回收,因为new出来的Object类的实例处于“可到达”状态,也就是能够通过引用找到它。所以一般需要obj = null;,消除掉这个“可到达”的引用,才会被GC顺利回收。

再如链表,销毁普通链表操作时,只需要将root元素置空(root = null;)即可,因为每个元素都保存了对下一个元素的引用,而下一个元素也只能通过上一个元素找到它。因此root置空后,root元素“无法到达”,被销毁;紧接着root连接着的元素由于root被销毁,指向它的引用消失,于是也会被GC回收,以此循环,最后将整条链表全部回收掉。

上面代码段中,栈内部维护着对这些对象的过期引用(obsolete reference),即永远也不会再被解除的引用。由于pop方法中元素出栈之后,栈内原位置的元素仍然存在(因为用的是数组,元素出栈后,出栈元素原来的位置其实仍然留有其引用)。过期引用指的是下标大于size的那一部分元素。

Java这种支持辣鸡回收的语言,内存泄漏(或称“无意识的对象保持”?)往往比较隐蔽,如果某对象被无意识地保存起来,那么GC就不会处理这个对象,也同样不会处理被这个对象所引用的其他对象。因此即使只有少量几个对象被无意识地保存下来,也会造成很多对象令GC无法处理,对性能造成巨大的潜在隐患。

对应措施

先展示如何改正上面代码中的错误情况。改正措施非常简单,只需这样

@Deprecated
    public Object badPop(){
        if (size == 0){
            throw new EmptyStackException();
        }
        return elements[--size];
    }

    public Object pop(){
        if (size == 0){
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        //解除对过期对象的引用,防止内存泄漏
        elements[size] = null;
        return result;
    }

每次pop()时对应元素出栈后,将栈内引用去掉就好(elements[size] = null;)

这样做的另一个好处是,可以防止使用其他对象时不小心使用到了这个本应被抛弃的对象,出现这种情况时会立即抛出空指针,而不是等我们注意到结果和预期不一致,再去辛苦地debug

需要注意的是,我们并不需要过分地小心,而对每个对象使用结束后都把它置空,这会把代码搞得很乱。清空过时对象引用应该是一种例外,而不是规范。消除过时对象引用的最好方式是让包含该对象的变量结束生命周期,只要变量声明的作用域范围设计得当,这种情况就会自然而然地发生。

那么什么时候需要考虑使用置空引用来防止内存泄漏?

1.类自己管理内存

分析{@link MemoryLeakTest}中造成内存泄漏的原因,该类选择使用Stack自己管理内存,存储池包含了elements数组(对象引用单元,而不是对象本身,类比c语言指针)的元素,而不是对象本身,数组中,下标号小于size的元素有效,而大于的则无效。但是GC并不知道这一点,它把整个数组的元素全部等同对待。这时候,需要程序员将这种情况告诉GC,即手动置空数组元素。

于是导出了情形1:

类自己管理内存时,程序员应当警惕内存泄漏。元素被释放掉时,该元素中的任何对象引用都要释放掉。

2.缓存

对象被放到缓存中后,很容易被忘掉。解决的办法是确定它何时才不再有意义,这时候就把它的对象引用清掉。根据具体情况有不同的解决方案:

  1. 只要在缓存之外存在对某个项的键的引用,该项就有意义;如果没有存在对某个项的键的引用,该项就没有意义。这时使用WeakHashMap来代表缓存,在外部手动取消对其引用,让GC自动将其回收
  2. 通过LinkedHashMap自带的removeEldestEntry实现在添加新条目时顺便清理

3.监听器等回调

Android中这种情况非常常见,因为网络通信时需要时间的,经常会有数据没返回回来,用户就点了返回把界面finish掉,而由于未与负责数据返回结果的监听器解除绑定,最后弹出空指针。这种情况可以考虑对监听器使用弱引用,也可以使用RxJava等设计好的工具,这里一并省略。

解决方案举例

假设我们需要在类中构建一个简单缓存以维护要缓存的对象,由于{@link WeakHashMap}的设计,“An entry in a WeakHashMap will automatically be removed when its key is no longer in ordinary use.”如果我们明确地知道什么时候不会再用到某一缓存内的对象,而且还知道这个对象所对的键(KEY),就可以使用Weak Preference代替缓存,那么我们可以在缓存之外手动取消该键的使用,这样就可以使它被自动回收。

但是,更多情况下什么时候不再使用某一元素是很难确定的,随着时间推移,缓存中的内容会变得越来越没价值,这种情况应该新开一个后台线程(Timer或者ScheduledThreadPoolExecutor),时不时清理掉没用的项,或者在给缓存添加新条目时顺便清理。

解决方案一

LinkedHashMap.removeEldestEntry(Map.Entry)会在新元素加入时,由LinkedHashMap.afterNodeInsertion(boolean)调用,默认返回false不令原有最老的元素被移除,我们可以通过重写这个方法,进而消除对过时对象的引用防止内存泄漏。

 *注:具体如何重写方式也有很多,这里removeEldestEntry其实是借鉴了LRU(最近最少使用)的思想,其他还有很多其他思路,比如FIFO(先进先出),NRU(非最近使用)等.如果学《操作系统》会有讲到,感兴趣也可以搜一搜

解决方案二:

这种常见的情况当然不会逃过JDK设计者大佬们的法眼,{@link WeakHashMap#expungeStaleEntries()}就是专门负责这个的,它去掉引用队列中所有失效的引用,并删除关联的映射,用引用队列代替周期性的对全部元素扫描,大多数Map操作会用到它。

public class SocketCache extends LinkedHashMap {

    /**
     * 配合{@link SocketCache}的user类
     */
    private class User {private String username;}

    private SocketCache(){}

    private static final SocketCache INSTANCE = new SocketCache();

    private static final Map<Socket,User> m = new WeakHashMap<Socket,User>();

    public static SocketCache getInstance(){
        return INSTANCE;
    }

    public void setUser(Socket s, User u) {
        m.put(s, u);
    }
    public User getUser(Socket s) {
        return m.get(s);
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        // 重写该方法以判断是否处理最老元素,
        // 比如 return size() > maxCapacity
        return super.removeEldestEntry(eldest);
    }
}

全代码git地址:点我点我

猜你喜欢

转载自blog.csdn.net/q2878948/article/details/81327387
今日推荐