JDK1.8源码逐字逐句带你理解WeakHashMap底层

来源:互联网 发布:知乎无法登陆 编辑:程序博客网 时间:2024/06/11 11:16

引言

WeakHashMap其实也是java不常见的东西,但是和linkedHashMap一样,有它自己独特的功能。在本篇博文中我会用例子详细介绍它独有的属性,同时会对照源码来解释为什么它具备这样的功能。在知识点中会扩展关于引用的相关知识,帮助后面的理解。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

技术点

1、java中的引用
关于java中的引用,其实我在“GC-谈谈“生死””这篇文章中就详细介绍过引用的概念,从原来的粗犷定义到现在的定义(http://blog.csdn.net/u012403290/article/details/65698856)。引用类型主要分为4种:①强引用;②软引用;③弱引用;④虚引用。强引用就是永远不会回收掉被引用的对象,比如说我们代码中new出来的对象。软引用表示有用但是非必需的,如果系统内存资源紧张,可能就会被回收;弱引用表示非必需的对象,只能存活到下一次垃圾回收发生之前;虚引用是最弱的,这个引用无法操作对象。在java中有与之对应的对象:SoftReference(软引用), WeakReference(弱引用),PhantomReference(虚引用)。在我们今天要研究的WeakHashMap中用WeakReference来实现。

2、引用队列ReferenceQueue
根据本人的理解,引用队列就相当于一个电话簿一样的东西,用于监听和管理在引用对象中被回收的对象。具体我用一段代码来解释:

    Object o1 = new Object();    Integer o2 = new Integer((int) o1);

比如说上面两段代码,在我们看来如果o2对象不被回收的话,o1永远都不可能被回收。但是在引用(Reference)中,存在这么一个情况:如果o1对象除了在o2中有引用之外没有别的地方存在引用,那么就可以回收o1。然后当这个o1被回收之后,我们就需要把o2放入引用队列中,所以引用队列(ReferenceQueue)就是Reference的监听器。在WeakHashMap中就是通过ReferenceQueue来反向处理map中的数据,如果对象被回收了,那么就需要把map中的对应数据移除。

一个例子

或许对于上面的介绍,很多人都看不懂的,接下来我先用HashMap建立一个例子帮大家理解:

package com.brickworkers;import java.lang.ref.ReferenceQueue;import java.lang.ref.WeakReference;import java.util.HashMap;import java.util.Map;public class ReferenceTest {    private static final int _1MB = 1024*1024;//设置大小为1MB    public static void main(String[] args) throws InterruptedException {        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();//用引用队列进行监控引用的回收情况        Object value = new Object();        Map<Object, Object> map = new HashMap<Object, Object>();        for (int i = 0; i < 100; i++) {//循环100次把数据插入到弱应用中(WeakReference), 同时把弱引用作为key存入HashMap            byte[] bytes = new byte[_1MB];            //每个引用中都有关联引用队列(referenceQueue)的构造器,用引用队列监听回收情况            //如此,那么每次WeakReference中的bytes被回收之后,那么这个weakReference对象就会放入引用队列            WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes, referenceQueue);            map.put(weakReference, value);        }        Thread thread = new Thread(new Runnable() {//线程通过调用引用队列的情况查看那些对象被回收            @SuppressWarnings("unchecked")            public void run() {                try {                    int cnt = 0;                    WeakReference<byte[]> k;                    while ((k = (WeakReference<byte[]>) referenceQueue.remove()) != null) {//返回被回收对象的引用(注意本例中被回收的是bytes)                        System.out.println((cnt++)+"回收了"+k);                        System.out.println("map的size = " + map.size());//用于监控map的存储数量有没有发生变化                    }                } catch (Exception e) {                    // TODO: handle exception                }            }        });        thread.start();    }}    //截取一部分输出://  53回收了java.lang.ref.WeakReference@232204a1//  map的size = 100//  54回收了java.lang.ref.WeakReference@355da254//  map的size = 100//  55回收了java.lang.ref.WeakReference@d716361//  map的size = 100//  56回收了java.lang.ref.WeakReference@74a14482//  map的size = 100//  57回收了java.lang.ref.WeakReference@12a3a380//  map的size = 100//  58回收了java.lang.ref.WeakReference@60e53b93//  map的size = 100

注意,回收的对象是bytes,并不是weakReference, 这也是为什么HashMap中的数据长度并没有发生变化的原因。我们再梳理一下运行流程:
1、bytes对象存入到weakReference对象中。
2、weakReference对象作为key,一个Object作为值存入HashMap中
2、GC回收了bytes对象,这个时候就要把引用这个对象的weakReference对象存储到ReferenceQueue中
3、死循环ReferenceQueue, 打印出被回收的对象。

下面就是上来的引用关系:
HashMap ——>weakReference——>byte[],千万注意被回收的是byte[]对象。
其实,上面这个逻辑就是核心WeakHashMap的实现,WeakHashMap只不过比上述的代码多了一步:把引用回收的对象从Map中移除罢了。

WeakHashMap来实现上面例子

package com.brickworkers;import java.util.Map;import java.util.WeakHashMap;public class ReferenceTest {    private static final int _1MB = 1024*1024;//设置大小为1MB    public static void main(String[] args) throws InterruptedException {        Object value = new Object();        Map<Object, Object> map = new WeakHashMap<Object, Object>();        for (int i = 0; i < 100; i++) {//循环100次把数据插入WeakHashMap中            byte[] bytes = new byte[_1MB];            map.put(bytes, value);        }        while (true) {//死循环监控map大小变化            Thread.sleep(500);//稍稍停顿,效果更直观            System.out.println(map.size());//打印WeakHashMap的大小            System.gc();//建议系统进行GC        }    }    //截取一部分输出://  41//  0//  0//  0}

以上的代码就是用WeakHashMap来实现了,你会说为什么不直接在最上面的代码把HashMap改成WeakHashMap就行了呢?不行的!WeakHashMap在类的内部就构建了引用队列(ReferenceQueue)和弱引用(weakReference ),具体的下面源码会介绍到。我们先来分析和解释一下上面的代码,一般人会有2个疑问:
①为什么我插入100个数据,第一次打印是41呢?
因为在插入的过程中已经触发过GC了,你可以把size的打印放到循环内部,你就会发现原因。同时为什么是到达41呢?这个和你的内存有关系,如果内存很富足,它就不会发生GC,而且弱引用是鸡肋一般的东西:食之无味,弃之可惜。他们只能存活到下次GC之前。
②为什么后来又变成0了呢?
因为在打印了大小之后,我建议系统(System.gc())发起一次GC操作,为什么说是建议呢?因为系统不一定会接收到你 指令就会发生GC的。一旦GC发生,那么弱引用就会被清除,导致WeakHashMap的大小为0。
同时,值得一提的是,存在WeakHashMap中的数据,并不会平白无故就给你移除了map中的数据,必然是你触发了一些操作,在上述代码中size方法就会触发这个操作,下面是size的源码:

    /**     * Returns the number of key-value mappings in this map.     * This result is a snapshot, and may not reflect unprocessed     * entries that will be removed before next attempted access     * because they are no longer referenced.     */    public int size() {        if (size == 0)            return 0;        expungeStaleEntries();//这个操作就是处理到不存在的引用方法        return size;    }

当然不仅仅在size方法会触发,下面源码介绍我们会讲到。

逐字逐句理解WeakHashMap

当然,理解Map相关的,需要你对Map有所了解,如果你不是很了解请参考一下我写的关于HashMap的博文:http://blog.csdn.net/u012403290/article/details/65442646。
1、在WeakHashMap中核心成员变量(关于HashMap中已存在的不再赘述):
①引用队列

    /**     * Reference queue for cleared WeakEntries     */    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

这个队列其实就是我们前面研究过的,用于监控对象回收的情况。

②静态内部类Entry K,V
下面是我截取的部分源码

    /**     * The entries in this hash table extend WeakReference, using its main ref     * field as the key.     */     //继承了弱引用WeakReference, 同时实现了Map.Entry接口    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;        }        .        .        .//这里还重写了一些方法        }

这个静态内部类也是WeakHashMap的核心,因为它把key值封装进了弱引用(WeakReference)中,这样一来,就回到了我们最前面的例子中,GC的时候可以回收掉弱引用对象中引用的对象(很拗口是不是?我自己写的自己读都拗口,其实就是真正的key值被弱引用WeakReference包装了),在源码中super实现了弱引用与引用队列关联的构造器,这样引用队列可以对弱引用进行监控了。

③核心移除map中K,V方法
如此一来,结合最前面的代码,我们对WeakHashMap的理解已经基本成型了。接下来,我们要解释一下为什么对象回收之后,map中的对应K,V也会被移除,核心就是下面这个方法:

 /**     * Expunges stale entries from the table.     */    private void expungeStaleEntries() {        for (Object x; (x = queue.poll()) != null; ) {//存在对象被GC, 那么就需要移除map中对应的数据            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) {//如果P节点存在                    Entry<K,V> next = p.next;//定义一个next节点指向p的下个节点                    if (p == e) {//如果P就是当前节点                        if (prev == e)                            table[i] = next;//意思就是桶中第一个数据就是需要移除的,直接把第二个节点放到头节点的位置                        else                            prev.next = next;//那就把上个节点的下个节点指向p后面的节点                        // Must not null out e.next;                        // stale entries may be in use by a HashIterator                        e.value = null; // Help GC帮助GC,直接删除e的对应value值                        size--;//减少WeakHashMap的大小                        break;//结束                    }                    prev = p;                    p = next;                }            }        }    }

或许有小伙伴看源代码有些吃力,我把上面这段代码主要做的事情写出来:
①:循环遍历引用队列(queue), 如果发现某个对象被GC了,那么就开始处理。
②:如果被处理的这个节点是头节点,那么直接把该节点的下个节点放到头节点,然后帮助GC去除value的引用,接着把WeakHashMap的大小减1。
③:如果被处理的这个节点不是头结点,那么就需要把这个节点的上个节点中的next指针直接指向当前节点的下个节点。意思就是a->b->c,这个时候要移除b,那么就变成a->c。然后帮助GC去除value的引用,接着把WeakHashMap的大小减1。

那么在那些时候出发这个expungeStaleEntries方法呢?查询源码之后就会发现好多方法都会调用这个方法:

//size方法    public int size() {        if (size == 0)            return 0;        expungeStaleEntries();//去除被回收的对象        return size;    }//getTable方法(这个方法是put和get方法的辅助方法)    /**     * Returns the table after first expunging stale entries.     */    private Entry<K,V>[] getTable() {        expungeStaleEntries();//去除被回收的对象        return table;    }//resize扩容方法    void resize(int newCapacity) {        Entry<K,V>[] oldTable = getTable();        int oldCapacity = oldTable.length;        if (oldCapacity == MAXIMUM_CAPACITY) {            threshold = Integer.MAX_VALUE;            return;        }        Entry<K,V>[] newTable = newTable(newCapacity);        transfer(oldTable, newTable);        table = newTable;        /*         * If ignoring null elements and processing ref queue caused massive         * shrinkage, then restore old table.  This should be rare, but avoids         * unbounded expansion of garbage-filled tables.         */        if (size >= threshold / 2) {            threshold = (int)(newCapacity * loadFactor);        } else {            expungeStaleEntries();//去除被回收的对象            transfer(newTable, oldTable);            table = oldTable;        }    }//get方法(基于上面说的getTable方法)    public V get(Object key) {        Object k = maskNull(key);        int h = hash(k);        Entry<K,V>[] tab = getTable();//getTable中包装了expungeStaleEntries方法        int index = indexFor(h, tab.length);        Entry<K,V> e = tab[index];        while (e != null) {            if (e.hash == h && eq(k, e.get()))                return e.value;            e = e.next;        }        return null;    }//put方法(基于上面说的getTable方法)    public V put(K key, V value) {        Object k = maskNull(key);        int h = hash(k);        Entry<K,V>[] tab = getTable();//getTable中包装了expungeStaleEntries方法        int i = indexFor(h, tab.length);        for (Entry<K,V> e = tab[i]; e != null; e = e.next) {            if (h == e.hash && eq(k, e.get())) {                V oldValue = e.value;                if (value != oldValue)                    e.value = value;                return oldValue;            }        }

这个方法是渗透在很多方法里面的,这里就不继续一一列举了,同时关于WeakHashMap的添加(put),获取(get), 扩容(resize)这里就不一一介绍了,如果不清楚的,请去查看我写的HashMap详解,里面我又详细介绍过。

如果你觉得我那里说的不对,或者有更好的解释,欢迎留言交流。我说的并不一定对,可能只是一个简单的指导作用,大家可以自己深入研究一下,或许会有别开新面的收获哦。

3 0