从WeakHashMap开始

原创文章 转载需注明出处
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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容