WeakHashMap源码分析
前言:WeakHashMap可能平时使用的频率并不高,但是你可能听过WeakHashMap会进行自动回收吧,下面就对其原理进行分析。
注:本文jdk源码版本为jdk1.8.0_172
1.WeakHashMap介绍
WeakHashMap是一种弱引用的map,底层数据结构为数组+链表,内部的key存储为弱引用,在GC时如果key不存在强引用的情况下会被回收掉,而对于value的回收会在下一次操作map时回收掉,所以WeakHashMap适合缓存处理。
1 java.lang.Object 2 ↳ java.util.AbstractMap<K, V> 3 ↳ java.util.WeakHashMap<K, V> 4 5 public class WeakHashMap<K,V> 6 extends AbstractMap<K,V> 7 implements Map<K,V> {}
从WeakHashMap的继承关系上来看,可知其继承AbstractMap,实现了Map接口。其底层数据结构是Entry数组,Entry的数据结构如下:
从源码上可知,Entry的内部并没有存储key的值,而是通过调用父类的构造方法,传入key和ReferenceQueue,最终key和queue会关联到Reference中,这里是GC时,清清除key的关键,这里大致看下Reference的源码:
1 private static class ReferenceHandler extends Thread { 2 3 private static void ensureClassInitialized(Class<?> clazz) { 4 try { 5 Class.forName(clazz.getName(), true, clazz.getClassLoader()); 6 } catch (ClassNotFoundException e) { 7 throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e); 8 } 9 } 10 11 static { 12 // pre-load and initialize InterruptedException and Cleaner classes 13 // so that we don't get into trouble later in the run loop if there's 14 // memory shortage while loading/initializing them lazily. 15 ensureClassInitialized(InterruptedException.class); 16 ensureClassInitialized(Cleaner.class); 17 } 18 19 ReferenceHandler(ThreadGroup g, String name) { 20 super(g, name); 21 } 22 23 public void run() { 24 // 注意这里为一个死循环 25 while (true) { 26 tryHandlePending(true); 27 } 28 } 29 } 30 static boolean tryHandlePending(boolean waitForNotify) { 31 Reference<Object> r; 32 Cleaner c; 33 try { 34 synchronized (lock) { 35 if (pending != null) { 36 r = pending; 37 // 'instanceof' might throw OutOfMemoryError sometimes 38 // so do this before un-linking 'r' from the 'pending' chain... 39 c = r instanceof Cleaner ? (Cleaner) r : null; 40 // unlink 'r' from 'pending' chain 41 pending = r.discovered; 42 r.discovered = null; 43 } else { 44 // The waiting on the lock may cause an OutOfMemoryError 45 // because it may try to allocate exception objects. 46 if (waitForNotify) { 47 lock.wait(); 48 } 49 // retry if waited 50 return waitForNotify; 51 } 52 } 53 } catch (OutOfMemoryError x) { 54 // Give other threads CPU time so they hopefully drop some live references 55 // and GC reclaims some space. 56 // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above 57 // persistently throws OOME for some time... 58 Thread.yield(); 59 // retry 60 return true; 61 } catch (InterruptedException x) { 62 // retry 63 return true; 64 } 65 66 // Fast path for cleaners 67 if (c != null) { 68 c.clean(); 69 return true; 70 } 71 // 加入对列 72 ReferenceQueue<? super Object> q = r.queue; 73 if (q != ReferenceQueue.NULL) q.enqueue(r); 74 return true; 75 } 76 77 static { 78 ThreadGroup tg = Thread.currentThread().getThreadGroup(); 79 for (ThreadGroup tgn = tg; 80 tgn != null; 81 tg = tgn, tgn = tg.getParent()); 82 // 创建handler 83 Thread handler = new ReferenceHandler(tg, "Reference Handler"); 84 /* If there were a special system-only priority greater than 85 * MAX_PRIORITY, it would be used here 86 */ 87 // 线程优先级最大 88 handler.setPriority(Thread.MAX_PRIORITY); 89 // 设置为守护线程 90 handler.setDaemon(true); 91 handler.start(); 92 93 // provide access in SharedSecrets 94 SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() { 95 @Override 96 public boolean tryHandlePendingReference() { 97 return tryHandlePending(false); 98 } 99 }); 100 }
通过查看Reference源码可知,在实例化时会创建一个守护线程,然后不断循环将GC时的Entry入队,关于如何清除value值的下面会进行分析。
2.具体源码分析
put操作:
1 public V put(K key, V value) { 2 // 确定key值,允许key为null 3 Object k = maskNull(key); 4 // 获取器hash值 5 int h = hash(k); 6 // 获取tab 7 Entry<K,V>[] tab = getTable(); 8 // 确定在tab中的位置 简单的&操作 9 int i = indexFor(h, tab.length); 10 // 遍历,是否要进行覆盖操作 11 for (Entry<K,V> e = tab[i]; e != null; e = e.next) { 12 if (h == e.hash && eq(k, e.get())) { 13 V oldValue = e.value; 14 if (value != oldValue) 15 e.value = value; 16 return oldValue; 17 } 18 } 19 20 // 修改次数自增 21 modCount++; 22 // 取出i上的元素 23 Entry<K,V> e = tab[i]; 24 // 构建链表,新元素在链表头 25 tab[i] = new Entry<>(k, value, queue, h, e); 26 // 检查是否需要扩容 27 if (++size >= threshold) 28 resize(tab.length * 2); 29 return null; 30 }
分析:
WeakHashMap的put操作与HashMap相似,都会进行覆盖操作(相同key),但是注意插入新节点是放在链表头。上述代码中还要一个关键的函数getTable,后面会对其进行具体分析,先记下。
get操作:
1 public V get(Object key) { 2 // 确定key 3 Object k = maskNull(key); 4 // 计算其hashCode 5 int h = hash(k); 6 Entry<K,V>[] tab = getTable(); 7 int index = indexFor(h, tab.length); 8 // 获取对应位置上的元素 9 Entry<K,V> e = tab[index]; 10 while (e != null) { 11 // 如果hashCode相同,并且key也相同,则返回,否则继续循环 12 if (e.hash == h && eq(k, e.get())) 13 return e.value; 14 e = e.next; 15 } 16 // 未找到,则返回null 17 return null; 18 }
分析:
get操作逻辑简单,根据key遍历对应元素即可。
remove操作:
1 public V remove(Object key) { 2 Object k = maskNull(key); 3 int h = hash(k); 4 Entry<K,V>[] tab = getTable(); 5 int i = indexFor(h, tab.length); 6 // 数组上第一个元素 7 Entry<K,V> prev = tab[i]; 8 Entry<K,V> e = prev; 9 // 循环 10 while (e != null) { 11 Entry<K,V> next = e.next; 12 // 如果hash值相同,并且key一样,则进行移除操作 13 if (h == e.hash && eq(k, e.get())) { 14 // 修改次数自增 15 modCount++; 16 // 元素个数自减 17 size--; 18 // 如果就是头元素,则直接移除即可 19 if (prev == e) 20 tab[i] = next; 21 else 22 // 否则将前驱元素的next赋值为next,则将e移除 23 prev.next = next; 24 return e.value; 25 } 26 // 更新prev和e,继续循环 27 prev = e; 28 e = next; 29 } 30 return null; 31 }
分析:
移除元素操作的整体逻辑并不复杂,就是进行链表的常规操作,注意元素是链表头时的特别处理,通过上述注释,理解应该不困难。
resize操作(WeakHashMap的扩容操作)
1 void resize(int newCapacity) { 2 Entry<K,V>[] oldTable = getTable(); 3 // 原数组长度 4 int oldCapacity = oldTable.length; 5 if (oldCapacity == MAXIMUM_CAPACITY) { 6 threshold = Integer.MAX_VALUE; 7 return; 8 } 9 // 创建新的数组 10 Entry<K,V>[] newTable = newTable(newCapacity); 11 // 数据转移 12 transfer(oldTable, newTable); 13 table = newTable; 14 15 /* 16 * If ignoring null elements and processing ref queue caused massive 17 * shrinkage, then restore old table. This should be rare, but avoids 18 * unbounded expansion of garbage-filled tables. 19 */ 20 // 确定扩容阈值 21 if (size >= threshold / 2) { 22 threshold = (int)(newCapacity * loadFactor); 23 } else { 24 // 清除被GC的value 25 expungeStaleEntries(); 26 // 数组转移 27 transfer(newTable, oldTable); 28 table = oldTable; 29 } 30 } 31 32 private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) { 33 // 遍历原数组 34 for (int j = 0; j < src.length; ++j) { 35 // 取出元素 36 Entry<K,V> e = src[j]; 37 src[j] = null; 38 // 链式找元素 39 while (e != null) { 40 Entry<K,V> next = e.next; 41 Object key = e.get(); 42 // key被回收的情况 43 if (key == null) { 44 e.next = null; // Help GC 45 e.value = null; // " " 46 size--; 47 } else { 48 // 确定在新数组的位置 49 int i = indexFor(e.hash, dest.length); 50 // 插入元素 注意这里为头插法,会倒序 51 e.next = dest[i]; 52 dest[i] = e; 53 } 54 e = next; 55 } 56 } 57 }
分析:
WeakHashMap的扩容函数中有点特别,因为key可能被GC掉,所以在扩容时也许要考虑这种情况,其他并没有什么特别的,通过以上注释理解应该不难。
在以上源码分析中多次出现一个函数:expungeStaleEntries
1 private void expungeStaleEntries() { 2 // 从队列中取出被GC的Entry 3 for (Object x; (x = queue.poll()) != null; ) { 4 synchronized (queue) { 5 @SuppressWarnings("unchecked") 6 Entry<K,V> e = (Entry<K,V>) x; 7 // 确定元素在队列中的位置 8 int i = indexFor(e.hash, table.length); 9 // 取出数组中的第一个元素 prev 10 Entry<K,V> prev = table[i]; 11 Entry<K,V> p = prev; 12 // 循环 13 while (p != null) { 14 Entry<K,V> next = p.next; 15 // 找到 16 if (p == e) { 17 // 判断是否是链表头元素 第一次时 18 if (prev == e) 19 // 将next直接挂在i位置 20 table[i] = next; 21 else 22 // 进行截断 23 prev.next = next; 24 // Must not null out e.next; 25 // stale entries may be in use by a HashIterator 26 e.value = null; // Help GC 27 size--; 28 break; 29 } 30 // 更新prev和p 31 prev = p; 32 p = next; 33 } 34 } 35 } 36 }
分析:
该函数的主要作用就是清除Entry的value,该Entry是在GC清除key的过程中入队的。函数的逻辑并不复杂,通过上述注释理解应该不难。
接下来看下该函数会在什么时候调用:
从以上调用链可知,在获取size(获取WeakHashMap的元素个数)和resize(扩容时)会调用该函数清除被GC的key对应的value值。但还有一个getTable函数也会调用该函数:
从以上调用链可知,在get/put等操作中都会调用expungeStaleEntries函数进GC后的收尾工作,其实WeakHashMap清除无强引用的核心也就是该函数了,因此理解该函数的作用是非常重要的。
3.总结
#1.WeakHashMap非同步,默认容量为16,扩容因子默认为0.75,底层数据结构为Entry数组(数组+链表)。
#2.WeakHashMap中的弱引用key会在下一次GC被清除,注意只会清除key,value会在每次map操作中清除。
#3.在WeakHashMap中强引用的key是不会被GC清除的。
by Shawn Chen,2019.09.09日,上午。