zoukankan      html  css  js  c++  java
  • 话说ReferenceQueue

    也是几年前写的,在内部邮件列表里发过,在这里保存一下。

    看到了这篇帖子: 《WeakHashMap的神话》http://www.javaeye.com/topic/587995
    因为Javaeye回帖还要先做个论坛小测验,所以懒得在上面回复了,在这里说下。

    以前设计缓存时也曾过用WeakHashMap来实现,对Java的Reference稍做过一些了解,其实这个问题,归根到底,是个Java GC的问题,由垃圾回收器与ReferenceQueue的交互方式决定的。WeakHashMap的实现也是通过ReferenceQueue这个“监听器”来优雅的实现自动删除那些引用不可达的key的。

    先看看ReferenceQueue在Java中的描述:

    Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected. 

    中文JavaDoc的描述:引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中

    查看源代码会发现它很简单,实现了一个队列的入队(enqueue)和出队(poll还有remove)操作,内部元素就是泛型的Reference,并且Queue的实现,是由Reference自身的链表结构所实现的。

    再来看 Reference类的代码,注意,javadoc中有一句,提到了它与GC是紧密相关的:

    Because reference objects are implemented in close cooperation with the garbage collector, this class may not be subclassed directly.

    从数据结构上看,Reference链表结构内部主要的成员有

    private T referent; //就是它所指引的
    Reference next;  //指向下一个;
    

    另一个比较重要的内部数据是:

    ReferenceQueue<? super T> queue;
    

    这个queue是通过构造函数传入的,表示创建一个Reference时,要将其注册到那个queue上。

    Queue的另一个作用是可以区分不同状态的Reference。Reference有4种状态,不同状态的reference其queue也不同:

    1. Active:

       queue = ReferenceQueue with which instance is registered,
       or ReferenceQueue.NULL if it was not registered with a queue; next = null.
      
    2. Pending:

       queue = ReferenceQueue with which instance is registered;
       next = Following instance in queue, or this if at end of list.
      
    3. Enqueued:

       queue = ReferenceQueue.ENQUEUED; next = Following instance 
       in queue, or this if at end of list.
      
    4. Inactive:

       queue = ReferenceQueue.NULL; next = this.
      

    那么,当我们创建了一个WeakReference,并且将其referent改变后,究竟发生了什么?先看一段代码:

    // eg1
    public static void test() throws Exception{
        Object o = new Object();
        // 默认的构造函数,会使用ReferenceQueue.NULL 作为queue
        WeakReference<Object> wr = new WeakReference<Object>(o);
        System.out.println(wr.get() == null);
        o = null;
        System.gc();
        System.out.println(wr.get() == null);
    }
    

    结果大家都知道,但其内部是怎么实现的,还需重新看Reference的源码,内部有两点需要注意:

    1)pending和 discovered成员:

    先看pending对象

    /* List of References waiting to be enqueued.  The collector adds
     * References to this list, while the Reference-handler thread removes
     * them.  This list is protected by the above lock object.
     */
    private static Reference pending = null;
    
    //这个对象,定义为private,并且全局没有任何给它赋值的地方,
    //根据它上面的注释,我们了解到这个变量是和垃圾回收期打交道的。
    

    再看discovered,同样为private,上下文也没有任何地方使用它

    transient private Reference<T> discovered;    /* used by VM */
    //看到了它的注释也明确写着是给VM用的。
    

    上面两个变量对应在VM中的调用,可以参考openjdk中的hotspot源码,在hotspot/src/share/vm/memory/referenceProcessor.cpp 的ReferenceProcessor::discover_reference 方法。(根据此方法的注释由了解到虚拟机在对Reference的处理有ReferenceBasedDiscoveryRefeferentBasedDiscovery两种策略)

    2)ReferenceHandler线程

    这个线程在Reference类的static构造块中启动,并且被设置为高优先级和daemon状态。此线程要做的事情,是不断的检查pending 是否为null,如果pending不为null,则将pending进行enqueue,否则线程进入wait状态。

    通过这2点,我们来看整个过程:

    pending是由jvm来赋值的,当Reference内部的referent对象的可达状态改变时,jvm会将Reference对象放入pending链表。

    结合代码eg1中的 o = null; 这一句,它使得o对象满足垃圾回收的条件,并且在后边显式的调用了System.gc(),垃圾收集进行的时候会标记WeakReference所referent的对象o为不可达(使得wr.get()==null),并且通过 赋值给pending,触发ReferenceHandler线程处理pending

    ReferenceHandler线程要做的是将pending对象enqueue,但默认我们所提供的queue,也就是从构造函数传入的是null,实际是使用了ReferenceQueue.NULLHandler线程判断queue为ReferenceQueue.NULL则不进行操作,只有非ReferenceQueue.NULL的queue才会将Reference进行enqueue。

    ReferenceQueue.NULL相当于我们提供了一个空的Queue去监听垃圾回收器给我们的反馈,并且对这种反馈不做任何处理。要处理反馈,则必须要提供一个非ReferenceQueue.NULL的queue。

    WeakHashMap则在内部提供了一个非NULL的ReferenceQueue

    private final ReferenceQueue<K> queue = new ReferenceQueue<K>();
    

    在 WeakHashMap 添加一个元素时,会使用 此queue来做监听器。见put方法中的下面一句:

        tab[i] = new Entry<K,V>(k, value, queue, h, e);
    

    这里Entry是一个内部类,继承了WeakReference

    class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V>
    

    WeakHashMap的 put, size, clear 都会间接或直接的调用到 expungeStaleEntries()方法。

    expungeStaleEntries顾名思义,此方法的作用就是将 queue中陈旧的Reference进行删除,因为其内部的referent都已经不可达了。所以也将这个WeakReference包装的key从map中删除。

    个人认为:ReferenceQueue是作为 JVM GC与上层Reference对象管理之间的一个消息传递方式,它使得我们可以对所监听的对象引用可达发生变化时做一些处理,WeakHashMap正是利用此来实现的。用图来大致表示如下:

    现在,我们再回到那个帖子的问题:http://www.javaeye.com/topic/587995

    他开始的测试写法为:

    List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<WeakHashMap<byte[][], byte[][]>>();
    for (int i = 0; i < 1000; i++) {
        WeakHashMap<byte[][], byte[][]> d = new WeakHashMap<byte[][], byte[][]>();
        d.put(new byte[1000][1000], new byte[1000][1000]);
        maps.add(d);
        System.gc();
        System.err.println(i);
    }
    

    会造成OOM异常。

    注意一下,他在for循环里每次都 new 一个新的WeakHashMap,并且key和value都是大对象,之后,他在 for循环的最后增加了一句访问 WeakHashMap的size(),使得不会造成OOM。

    首先上面的代码并不是没有执行GC,而是仅对 WeakHashMap中的key中的byte数组进行了回收,而value依然保持。我们可以先做个试验,把上面的value用小对象代替

    for (int i = 0; i < 10000; i++) {
        WeakHashMap<byte[][], Object> d = new WeakHashMap<byte[][], Object>();
        d.put(new byte[1000][1000], new Object());
        maps.add(d);
        System.gc();
        System.err.println(i);
    }
    

    上面的代码,即使执行10000次也没有问题,证明key中的byte数组确实被回收了。
    那为何key中的referent的数据被GC,却没有触发WeakHashMap去做清除整个key的操作呢?

    因为他for循环中每次都new一个新的WeakHashMap,在put操作后,虽然GC将WeakReference的key中的byte数组回收了,并将事件通知到了ReferenceQueue,但后续却没有相应的动作去触发 WeakHashMap 去处理 ReferenceQueue,所以 WeakReference 包装的key依然存在在WeakHashMap中,其对应的value也当然存在。

    而在for循环的尾巴增加了一句 d.size()方法,却可以了,是因为

    size()里面触发了expungeStaleEntries 操作,它将 ReferenceQueue中的 WeakReference对象从map中删除了,对应着value也一并删除了,使得value也被GC回收了。

  • 相关阅读:
    linux之awk命令
    HDU 2097 Sky数 进制转换
    HDU 2077 汉诺塔IV
    HDU 2094 产生冠军 dfs加map容器
    HDU 2073 叠框
    HDU 2083 简易版之最短距离
    HDU 2063 过山车 二分匹配
    天梯 1014 装箱问题
    天梯 1214 线段覆盖
    天梯 1098 均分纸牌
  • 原文地址:https://www.cnblogs.com/daichangya/p/12958919.html
Copyright © 2011-2022 走看看