提示七 消除过期的对象引用

2022-05-11  本文已影响0人  飞絮搅青冥

今天看第七条: 消除过期的对象引用。

作者先给出了一个栈的简单实现,其中pop方法直接移动数组指针的位置,然后说这样的栈在增长后收缩,那么弹栈的对象不会被垃圾回收,可能会存在内存泄露。解决的方法也很简单,在pop操作移动指针以后,把弹出元素的位置清空引用即可。我感觉这样的操作一般情况下都没什么必要,因为如果栈是活跃的,那么本身这个位置也会在之后被其他元素占据,如果栈不活跃,那么我们更应该思考是否需要用这个栈或者是否可以尽早结束整个栈的引用。不过作者后面也说清空对象引用应该是例外而不是规范。消除过期引用的最好方法是让包含引用的变量超出范围。一旦数组中的元素变成非活动的一部分,就可以手动清空这些元素的引用。

接着作者给出观点:一般来说,当一个类自己管理内存时,程序员应该警惕内存泄漏问题。 每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。这次作者给出了缓存的例子:只要在缓存之外存在对某个项(entry)的键(key)引用,那么这项就是明确有关联的,就可以用WeakHashMap 来表示缓存;这些项在过期之后自动删除。记住,只有当缓存中某个项的生命周期是由外部引用到键(key)而不是值(value)决定时,WeakHashMap 才有用。

我们接触到的引用类型一般都是强引用。强引用的特性是只要有强引用存在,被引用的对象就不会被垃圾回收。软引用SoftReference继承自Reference。一个对象如果只有软引用,java gc在内存充足的时候不会回收它,内存不足的时候就会被回收。弱引用weakReference和softReference很类似,不同的是weekReference引用的对象只要垃圾回收执行,就会被回收,而不管是否内存不足。虚引用PhantomReference每次get都是null,需要和ReferenceQueue搭配使用,一般代码中都不需要。WeakHashMap跟WeakReference有点类似,在WeakHashMap如果key不再被使用,被赋值为null的时候,该key对应的Entry会自动从WeakHashMap中删除。

    @Test
    public void useWeakHashMap(){
        WeakHashMap<Object, Object> map = new WeakHashMap<>();
        Object key1= new Object();
        Object value1= new Object();
        Object key2= new Object();
        Object value2= new Object();

        map.put(key1, value1);
        map.put(key2, value2);
        System.out.println(map);

        key1 = null;
        System.gc();
        System.out.println(map);
    }
    
    {java.lang.Object@6bf2d08e=java.lang.Object@5eb5c224,java.lang.Object@5fcfe4b2=java.lang.Object@53e25b76}
    {java.lang.Object@6bf2d08e=java.lang.Object@5eb5c224}

接着,作者又说了更常见的情况是,缓存项有用的生命周期不太明确,随着时间的推移一些项变得越来越没有价值。在这种情况下,缓存应该偶尔清理掉已经废弃的项。这可以通过一个后台线程 (也许是ScheduledThreadPoolExecutor ) 或将新的项添加到缓存时顺便清理。LinkedHashMap类使用它的removeEldestEntry方法实现了后一种方案。LinkedHashMap我知道大概就是双向链表和HashMap合二为一,但是作者介绍的方法我确实没有了解过。简单了解一番以后,原来LinkedHashMap基本实现了LRU算法,当新元素被插入时,会被默认插入队尾,只要重写了removeEldestEntry就能用LinkedHashMap轻松实现LRU的缓存。

    @Test
    public void testLinkedHashMap() {
        final int MAX_S = 5;
        Map < Integer, String > map = new LinkedHashMap< Integer, String >() {
            protected boolean removeEldestEntry(Map.Entry < Integer, String > eldest) {
                return size() > MAX_S;
            }
        };
        map.put(1, "C");
        map.put(2, "C++");
        map.put(3, "JAVA");
        map.put(4, "PHP");
        map.put(5, "PYTHON");
        System.out.println("LinkedHashMap: " + map);
        map.put(6, "VUE");
        System.out.println("LinkedHashMap: " + map);
    }
    LinkedHashMap: {1=C, 2=C++, 3=JAVA, 4=PHP, 5=PYTHON}
    LinkedHashMap: {2=C++, 3=JAVA, 4=PHP, 5=PYTHON, 6=VUE}

作者认为最后一种常见的内存泄漏来源是监听器和其他回调。如果你实现了一个 API,其客户端注册回调,但是没有显式地撤销注册回调,除非采取一些操作,否则它们将会累积。确保回调立即被当做是垃圾回收的最佳方法是只存储它们的弱引用,例如,仅将它们保存在 WeakHashMap 的键中。因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。 通常仅在仔细的代码检查或借助堆分析器(heap profiler)的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的。

确实内存泄漏在TMS中比较少见,但是每次出现都需要大费周章去使用工具分析具体原因,所以我们在处理容易出这类问题的例子时需要格外小心。

上一篇下一篇

猜你喜欢

热点阅读