Java中的WeakHashMap源码分析
作者:weixin_30692143
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的源码:
private static class ReferenceHandler extends Thread { private static void ensureClassInitialized(Class<?> clazz) { try { Class.forName(clazz.getName(), true, clazz.getClassLoader()); } catch (ClassNotFoundException e) { throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e); } } static { // pre-load and initialize InterruptedException and Cleaner classes // so that we don't get into trouble later in the run loop if there's // memory shortage while loading/initializing them lazily. ensureClassInitialized(InterruptedException.class); ensureClassInitialized(Cleaner.class); } ReferenceHandler(ThreadGroup g, String name) { super(g, name); } public void run() { // 注意这里为一个死循环 while (true) { tryHandlePending(true); } } } 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; } static { ThreadGroup tg = Thread.currentThread().getThreadGroup(); for (ThreadGroup tgn = tg; tgn != null; tg = tgn, tgn = tg.getParent()); // 创建handler Thread handler = new ReferenceHandler(tg, "Reference Handler"); /* If there were a special system-only priority greater than * MAX_PRIORITY, it would be used here */ // 线程优先级最大 handler.setPriority(Thread.MAX_PRIORITY); // 设置为守护线程 handler.setDaemon(true); handler.start(); // provide access in SharedSecrets SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() { @Override public boolean tryHandlePendingReference() { return tryHandlePending(false); } }); }
通过查看Reference源码可知,在实例化时会创建一个守护线程,然后不断循环将GC时的Entry入队,关于如何清除value值的下面会进行分析。
2.具体源码分析
put操作:
public V put(K key, V value) { // 确定key值,允许key为null Object k = maskNull(key); // 获取器hash值 int h = hash(k); // 获取tab Entry<K,V>[] tab = getTable(); // 确定在tab中的位置 简单的&操作 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; } } // 修改次数自增 modCount++; // 取出i上的元素 Entry<K,V> e = tab[i]; // 构建链表,新元素在链表头 tab[i] = new Entry<>(k, value, queue, h, e); // 检查是否需要扩容 if (++size >= threshold) resize(tab.length * 2); return null; }
分析:
WeakHashMap的put操作与HashMap相似,都会进行覆盖操作(相同key),但是注意插入新节点是放在链表头。
上述代码中还要一个关键的函数getTable,后面会对其进行具体分析,先记下。
get操作:
public V get(Object key) { // 确定key Object k = maskNull(key); // 计算其hashCode int h = hash(k); Entry<K,V>[] tab = getTable(); int index = indexFor(h, tab.length); // 获取对应位置上的元素 Entry<K,V> e = tab[index]; while (e != null) { // 如果hashCode相同,并且key也相同,则返回,否则继续循环 if (e.hash == h && eq(k, e.get())) return e.value; e = e.next; } // 未找到,则返回null return null; }
分析:
get操作逻辑简单,根据key遍历对应元素即可。
remove操作:
public V remove(Object key) { Object k = maskNull(key); int h = hash(k); Entry<K,V>[] tab = getTable(); int i = indexFor(h, tab.length); // 数组上第一个元素 Entry<K,V> prev = tab[i]; Entry<K,V> e = prev; // 循环 while (e != null) { Entry<K,V> next = e.next; // 如果hash值相同,并且key一样,则进行移除操作 if (h == e.hash && eq(k, e.get())) { // 修改次数自增 modCount++; // 元素个数自减 size--; // 如果就是头元素,则直接移除即可 if (prev == e) tab[i] = next; else // 否则将前驱元素的next赋值为next,则将e移除 prev.next = next; return e.value; } // 更新prev和e,继续循环 prev = e; e = next; } return null; }
分析:
移除元素操作的整体逻辑并不复杂,就是进行链表的常规操作,注意元素是链表头时的特别处理,通过上述注释,理解应该不困难。
resize操作(WeakHashMap的扩容操作)
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 { // 清除被GC的value expungeStaleEntries(); // 数组转移 transfer(newTable, oldTable); table = oldTable; } } private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) { // 遍历原数组 for (int j = 0; j < src.length; ++j) { // 取出元素 Entry<K,V> e = src[j]; src[j] = null; // 链式找元素 while (e != null) { Entry<K,V> next = e.next; Object key = e.get(); // key被回收的情况 if (key == null) { e.next = null; // Help GC e.value = null; // " " size--; } else { // 确定在新数组的位置 int i = indexFor(e.hash, dest.length); // 插入元素 注意这里为头插法,会倒序 e.next = dest[i]; dest[i] = e; } e = next; } } }
分析:
WeakHashMap的扩容函数中有点特别,因为key可能被GC掉,所以在扩容时也许要考虑这种情况,其他并没有什么特别的,通过以上注释理解应该不难。
在以上源码分析中多次出现一个函数:expungeStaleEntries
private void expungeStaleEntries() { // 从队列中取出被GC的Entry 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); // 取出数组中的第一个元素 prev 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) // 将next直接挂在i位置 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 prev = p; p = next; } } } }
分析:
该函数的主要作用就是清除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清除的。
到此这篇关于Java中的WeakHashMap源码分析的文章就介绍到这了,更多相关Java的WeakHashMap内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!