Java思考

从WeakHashMap开始

2019-04-14  本文已影响0人  AlexSun1995

原创文章 转载需注明出处
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;
                }
            }
        }
    }

问题分析

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;
        }

总结与联想

  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
上一篇下一篇

猜你喜欢

热点阅读