Java集合包(八)——Map实现类之 WeakHashMap 原理分析

Java集合包(八)——Map实现类之 WeakHashMap 原理分析

转载请注明原文地址:https://www.cnblogs.com/ygj0930/p/13575843.html 

、WeakHashMap的重要成员

    1)table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。

      WeakHashMap的存储原理与HashMap是一样的,都是通过“链表+红黑树”的策略。

      不同的地方在于Entry的实现。

    private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V> {
        private V value;
        private final int hash;
        // 指向下一个节点
        private Entry<K,V> next;

        // 构造函数。
        Entry(K key, V value,
          ReferenceQueue<K> queue,
              int hash, Entry<K,V> next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }

        public K getKey() {
            return WeakHashMap.<K>unmaskNull(get());
        }
    
        ......
}

    WeakHashMap的Entry继承了弱引用 WeakReference,在Entry的构造函数中,它将key和WeakHashMap的引用队列queue作为参数,调用父类WeakReference的构造函数创建了一个弱引用,也就是说:key是作为弱引用的形式存在的。

    WeakReference的源码:

public class WeakReference<T> extends Reference<T> {
    public WeakReference(T referent) {
        super(referent);
    }
    
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
    
}

    

    2)queue保存的是“已被GC清除”的“弱引用的键”队列。

private final ReferenceQueue<K> queue = new ReferenceQueue<K>();

    弱引用类型的key的创建,需要传递 ReferenceQueue 作为构造参数,用它作为容器:如果弱引用key 所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

    然后这个队列中的key所对应的键值对就会在下一次Map扩容的时候被删除掉。

    3)其他成员与HashMap一样,例如:size、加载因子等。

二、关键函数

    1、清空table中的无用键值对:回收key已经没有在其他地方被引用的键值对。

    // 清空table中无用键值对。原理如下:
109     // (01) 当WeakHashMap中某个“弱引用的key”由于没有再被引用而被GC收回时,
110     //   被回收的“该弱引用key”也被会被添加到"ReferenceQueue(queue)"中。
111     // (02) 当我们执行expungeStaleEntries时,
112     //   就遍历"ReferenceQueue(queue)"中的所有key
113     //   然后就在“WeakReference的table”中删除与“ReferenceQueue(queue)中key”对应的键值对
114     private void expungeStaleEntries() {
115         Entry<K,V> e;
116         while ( (e = (Entry<K,V>) queue.poll()) != null) {
117             int h = e.hash;
118             int i = indexFor(h, table.length);
119 
120             Entry<K,V> prev = table[i];
121             Entry<K,V> p = prev;
122             while (p != null) {
123                 Entry<K,V> next = p.next;
124                 if (p == e) {
125                     if (prev == e)
126                         table[i] = next;
127                     else
128                         prev.next = next;
129                     e.next = null;  // Help GC
130                     e.value = null; //  "   "
131                     size--;
132                     break;
133                 }
134                 prev = p;
135                 p = next;
136             }
137         }
138     }

  2、put函数:在当前大小触及阈值时时扩容。

// 将“key-value”添加到WeakHashMap中
195     public V put(K key, V value) {
196         K k = (K) maskNull(key);
197         int h = HashMap.hash(k.hashCode());
198         Entry[] tab = getTable();
199         int i = indexFor(h, tab.length);
200 
201         for (Entry<K,V> e = tab[i]; e != null; e = e.next) {
202             // 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
203             if (h == e.hash && eq(k, e.get())) {
204                 V oldValue = e.value;
205                 if (value != oldValue)
206                     e.value = value;
207                 return oldValue;
208             }
209         }
210 
211         // 若“该key”对应的键值对不存在于WeakHashMap中,则将“key-value”添加到table中
212         modCount++;
213         Entry<K,V> e = tab[i];
214         tab[i] = new Entry<K,V>(k, value, queue, h, e);
215         if (++size >= threshold)
216             resize(tab.length * 2);
217         return null;
218     }

    

    3、扩容函数:将容量放大、调用清理函数删除无用的键值对。

// 重新调整WeakHashMap的大小,newCapacity是调整后的单位
221     void resize(int newCapacity) {
222         Entry[] oldTable = getTable();
223         int oldCapacity = oldTable.length;
224         if (oldCapacity == MAXIMUM_CAPACITY) {
225             threshold = Integer.MAX_VALUE;
226             return;
227         }
228 
229         // 新建一个newTable,将“旧的table”的全部元素添加到“新的newTable”中,
230         // 然后,将“新的newTable”赋值给“旧的table”。
231         Entry[] newTable = new Entry[newCapacity];
232         transfer(oldTable, newTable);
233         table = newTable;
234 
235         if (size >= threshold / 2) {
236             threshold = (int)(newCapacity * loadFactor);
237         } else {
238             // 删除table中“已被GC回收的key对应的键值对”
239             expungeStaleEntries();
240             transfer(newTable, oldTable);
241             table = oldTable;
242         }
243     }

  

三、WeakHashMap的键是“弱键”的真正理解

    首先我们要知道,Map接口的key都是引用类型,必须实现了hashCode和equals方法,不能是基本类型。

    因此,键值对中的键是引用类型来的,它指向一个对象。

    Java中的引用分为:强、软、弱、虚。【具体参见另一篇博文:https://www.cnblogs.com/ygj0930/p/6514114.html

    WeakHashMap的键是 弱引用 类型的,弱引用类型的特征是:如果一个对象只具有弱引用,则当GC扫描到该对象时就会回收它,不必等到内存不够时才回收;在回收之前我们可以通过get()方法获取对象,如果回收之后再调用get()方法就会返回null。

    因此,WeakHashMap的键是“弱键”的真正解读应该为:用来作为key的引用类型值所指向的对象,如果只剩下weakHashMap中的键值对中的键指向它,而在其他地方再无别的引用指向它时,则该对象被GC扫描到会被回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中【弱引用创建时可以传递一个引用队列进来】,而体现在weakHashMap则会被添加到WeakHashMap实例的 ReferenceQueue 队列中。当weakHashMap的当前大小触及阈值调用resize进行扩容时,就会调用 expungeStaleEntries() 函数,在该函数中遍历ReferenceQueue 队列,逐个删除weakHashMap中与引用队列中的key相对应的键值对。