美文网首页Java思考
从WeakHashMap开始

从WeakHashMap开始

作者: AlexSun1995 | 来源:发表于2019-04-14 17:10 被阅读0次

    原创文章 转载需注明出处
    2019/4/14

    序言

    HashMap对象只有在执行remove操作之后才会删除相应Entry. 参考下列代码, 即使k1=null并调用系统GC,hashmap的size仍然为2。HashMap的key是堆中对象的另一个强引用,与例中的k1独立。即使将k1设置为null,hashMap仍然持有一个强引用,从而并不会被GC算法标记清除。
    code1

     // hash map test
        public static void test3(){
            HashMap<String, Object> map = new HashMap<>();
            String k1 = new String("key1");
            String k2 = new String("key2");
            map.put(k1, new Object());
            map.put(k2, new Object());
            k1 = null;
            System.gc();
            printMap(map); // map的size仍然是2
        }
    

    通常在缓存场景下,hashMap会足够大。我们希望不常使用的key-value对能够被自动及时清除,WeakHashMap可以胜任此需求。不出意外的,下列代码map里key为k1的key-value对会被清除。
    code2

     public static void test4(){
            WeakHashMap<String, Object> map = new WeakHashMap<>();
            String k1 = new String("key1");
            String k2 = new String("key2");
            map.put(k1, new Object());
            map.put(k2, new Object());
            k1 = null;
            System.gc();
            printMap(map); // k1已经被删除了
        }
    

    本文以WeakHashMap为起点,涉及关键点有 Java引用,GC标记,弱引用,JVM
    简便起见,后文将WeakHashMap简称为WHM.

    WHM原理分析

    从实验结果开始分析,代码段2中,为什么将k1赋null以后并调用系统GC就可以清除WHM的key-value对?
    主体原因为两点:
    其一,如果堆区的对象在栈区中没有强引用,而仅仅只有弱引用,则该堆区对象会在GC算法运行时被清除。
    WHM实现上和HashMap的原理大部相同,但是WeakHashMap.Entry的继承了WeakReference类,当Entry的强引用不存在时,Entry的key被GC回收。
    其二,JVM的GC算法只清除Entry的KEY,value需要WHM自行处理,这种设计的原因后续分析。WHM在其关键方法expungeStaleEntries方法中清除value。
    上述表述中,第一条为JVM垃圾回收的职责,第二条为WHM职责;则WHM原理可归结为下述三点:

    1. JVM做了什么;如何做
    2. WHM做了什么
    3. JVM和WHM如何协作
      WeakHashMap.Entry的继承链为 Entry --> WeakReference --> Reference. Reference类实现了与GC合作的逻辑。
      code Entry类构造方法
     /**
             * Creates new entry.
             */
            Entry(Object key, V value,
                  ReferenceQueue<Object> queue,
                  int hash, Entry<K,V> next) {
                // ReferenceQuene
                super(key, queue);
                this.value = value;
                this.hash  = hash;
                this.next  = next;
            }
    

    Entry构造方法中调用WeakReference的父构造方法并传入一个ReferenceQueue实例queue,queue实例会传入Reference类。
    Reference中有字段pending:
    code3

    /* 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. The
         * list uses the discovered field to link its elements.
         */
        private static Reference<Object> pending = null;
    
    

    pending存储用于清除对象的引用链表,其在Reference实现中没有赋值逻辑,由JVM完成赋值。 Reference中开启一个后台线程用于监听pending,一旦不空将其添加到ReferenceQueue中。
    code4 在Reference的tryHandlePending方法中,被标记清除的pending入队。

       /**
         * Try handle pending {@link Reference} if there is one.<p>
         * Return {@code true} as a hint that there might be another
         * {@link Reference} pending or {@code false} when there are no more pending
         * {@link Reference}s at the moment and the program can do some other
         * useful work instead of looping.
         *
         * @param waitForNotify if {@code true} and there was no pending
         *                      {@link Reference}, wait until notified from VM
         *                      or interrupted; if {@code false}, return immediately
         *                      when there is no pending {@link Reference}.
         * @return {@code true} if there was a {@link Reference} pending and it
         *         was processed, or we waited for notification and either got it
         *         or thread was interrupted before being notified;
         *         {@code false} otherwise.
         */
        static boolean tryHandlePending(boolean waitForNotify) {
            Reference<Object> r;
            Cleaner c;
            try {
                synchronized (lock) {
                    if (pending != null) {
                        r = pending;
                        // 'instanceof' might throw OutOfMemoryError sometimes
                        // so do this before un-linking 'r' from the 'pending' chain...
                        c = r instanceof Cleaner ? (Cleaner) r : null;
                        // unlink 'r' from 'pending' chain
                        pending = r.discovered;
                        r.discovered = null;
                    } else {
                        // The waiting on the lock may cause an OutOfMemoryError
                        // because it may try to allocate exception objects.
                        if (waitForNotify) {
                            lock.wait();
                        }
                        // retry if waited
                        return waitForNotify;
                    }
                }
            } catch (OutOfMemoryError x) {
                // Give other threads CPU time so they hopefully drop some live references
                // and GC reclaims some space.
                // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
                // persistently throws OOME for some time...
                Thread.yield();
                // retry
                return true;
            } catch (InterruptedException x) {
                // retry
                return true;
            }
    
            // Fast path for cleaners
            if (c != null) {
                c.clean();
                return true;
            }
    
            ReferenceQueue<? super Object> q = r.queue;
            if (q != ReferenceQueue.NULL) q.enqueue(r);
            return true;
        }
    

    这里由于ReferenceQueue在WHM中实例化并被传入,则WHM中可以获取ReferenceQueue中被GC添加的引用。WHM的expungeStaleEntries方法消费ReferenceQueue中的元素,在table中找到元素对应的Entry并将其删除(基于拉链式实现的Map,删除逻辑类比链表删除)。
    需要注意的是,Entry的key对应的对象已经被删除,而e.value维持堆中对象的强引用,置为null以后就会在下次GC中被清除。
    code5

      /**
         * Expunges stale entries from the table.
         */
        private void expungeStaleEntries() {
            for (Object x; (x = queue.poll()) != null; ) {
                synchronized (queue) {
                    @SuppressWarnings("unchecked")
                        Entry<K,V> e = (Entry<K,V>) x;
                    int i = indexFor(e.hash, table.length);
    
                    Entry<K,V> prev = table[i];
                    Entry<K,V> p = prev;
                    while (p != null) {
                        Entry<K,V> next = p.next;
                        if (p == e) {
                            if (prev == e)
                                table[i] = next;
                            else
                                prev.next = next;
                            // Must not null out e.next;
                            // stale entries may be in use by a HashIterator
                            e.value = null; // Help GC
                            size--;
                            break;
                        }
                        prev = p;
                        p = next;
                    }
                }
            }
        }
    

    问题分析

    • WHM清除key引用而不清除value引用的实验验证
      code6
    public static void test5(){
            List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<>();
            for(int i=0; i<10000;i++){
               WeakHashMap<byte[][], byte[][]> weakHashMap = new WeakHashMap<>();
               weakHashMap.put(new byte[1000][1000], new byte[1][1]);
               maps.add(weakHashMap);
           }
        }
    

    code7

    public static void test5(){
            List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<>();
            for(int i=0; i<10000;i++){
               WeakHashMap<byte[][], byte[][]> weakHashMap = new WeakHashMap<>();
               weakHashMap.put(new byte[1000][1000], new byte[1000][1000]);
               maps.add(weakHashMap);
           }
        }
    

    code6并不会造成OOM,而 code7会。 说明WHM在没有触发expungeStaleEntries 时,会把key引用对应的堆区垃圾回收而不会对value引用对应的堆区垃圾回收。原因可有Entry构造函数看出,真正当做WeakReference的引用是是Enty的key而不是Entry全部。
    code WeakHashMap类

       /**
         * The entries in this hash table extend WeakReference, using its main ref
         * field as the key.
         */
        private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
            V value;
            final int hash;
            Entry<K,V> next;
            /**
             * Creates new entry.
             */
            Entry(Object key, V value,
                  ReferenceQueue<Object> queue,
                  int hash, Entry<K,V> next) {
                super(key, queue);
                this.value = value;
                this.hash  = hash;
                this.next  = next;
            }
    
    • WHM为什么只清除Entry.key引用的对象而不直接清除value引用对象?
      考虑一种假设场景:如果Entry的构造方法在调用WeakReference构造方法的时候reference参数传入的是Entry本身,而不是key,则在GC发生时Entry被直接删除。而由于Entry的链表式结构,一个Entry被删除之后其之后的Entry都将不可访问,这自然不是理想状态。WeakReference设计允许业务代码使用依赖注入的方法传入一个ReferenceQueue,ReferenceQueue中存放被GC清除对象的引用,业务代码可以用ReferenceQueue中被清除的对象引用个性化处理。当Java代码需要感知GC清除了哪些对象的时候,ReferenceQueue是Java代码和GC沟通的媒介。

    总结与联想

    1. 软引用(soft reference)是当堆空间不足时才会被GC回收;弱引用(weak Reference)则不管空间够不够都会被回收;但对象只要存在一个强引用就都不会被GC回收。 弱引用的这种特性在缓存场景中引用较多,因为开发者也很难确定指定缓存在何时被清除。
    2. WeakReference应用的另一个著名例子是ThreadLocal,Java中的每个Thread都持有一个ThreadLocalMap,ThreadLocal.set(T)事实上是将ThreadLocal本身作为key,T为value存储在当前Thread的ThreadLocalMap中。这种设计方法导致了ThreadLocalMap的生命周期和Thread一样,且引用被Thread持有,有潜在内存泄漏风险。因此ThreadLocalMap中的ThreadLocal key也设计为弱引用,当threadLocal不存在强引用时,Thread.ThreadLocalMap持有弱引用的ThreadLocal可以被清除(ThreadLocal仍然存在内存泄漏风险,使用时应小心)

    参考文章

    1. http://hongjiang.info/java-referencequeue/
    2. https://www.cnblogs.com/skywang12345/p/3311092.html

    相关文章

      网友评论

        本文标题:从WeakHashMap开始

        本文链接:https://www.haomeiwen.com/subject/grsswqtx.html