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

时间:2021-12-20 14:43:25

引言

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详解,里面我又详细介绍过。

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