相信很多人对WeakHashMap并没有完全理解。
WeakHashMap 持有的弱引用的 Key。
1. 弱引用的概念:
弱引用是用来描述非必需对象的,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
2. WeakHashMap中的弱引用
Key是如何被清除的?
WeakHashMap中的清除Key的核心方法:
private void expungeStaleEntries() { Entry<K,V> e; while ( (e = (Entry<K,V>) queue.poll()) != null) { int h = e.hash; int i = indexFor(h, table.length); 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) table[i] = next; else prev.next = next; e.next = null; // Help GC e.value = null; // " " size--; break; } prev = p; p = next; } } }
查看调用关系,可以看到几乎所有的方法都直接或间接的调用了该方法。但是查看WeakHashMap源码,并没有找到何时将Entry放入queue中。
那么queue队列中的Entry是如何来的?
Key弱引用是如何关联的?
毫无疑问,一定是在put元素的时候,key被设置为弱引用。
public V put(K key, V value) { K k = (K) maskNull(key); int h = HashMap.hash(k.hashCode()); Entry[] tab = getTable(); 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++; Entry<K,V> e = tab[i]; tab[i] = new Entry<K,V>(k, value, queue, h, e); //创建一个新节点 if (++size >= threshold) resize(tab.length * 2); return null; }
其中的queue为:
/** * Reference queue for cleared WeakEntries */ private final ReferenceQueue<K> queue = new ReferenceQueue<K>();
再来看一下Entry<K,V>的声明及构造函数:
private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V> {/** * Creates new entry. */ 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; } }
Entry<K,V>继承了WeakReference,并且在构造函数中将key 和 queue 提交给WeakReference,那么再来看一下WeakReference的构造函数:
public class WeakReference<T> extends Reference<T> {
//....
public WeakReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); } }
public abstract class Reference<T> {
private static Reference pending = null;
Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; }
}
现在答案就在Reference中。
打开Reference源码,可以看到一个静态块:
static { ThreadGroup tg = Thread.currentThread().getThreadGroup(); for (ThreadGroup tgn = tg;tgn != null;tg = tgn, tgn = tg.getParent()); 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(); }
其中for循环直到 获取到 JVM 线程组,使用JVM线程执行ReferenceHandler。
ReferenceHandler是Reference的内部类:
private static class ReferenceHandler extends Thread { ReferenceHandler(ThreadGroup g, String name) { super(g, name); } public void run() { for (;;) { Reference r; synchronized (lock) { if (pending != null) { r = pending; Reference rn = r.next; pending = (rn == r) ? null : rn; r.next = r; } else { try { lock.wait(); } catch (InterruptedException x) { } continue; } } // Fast path for cleaners if (r instanceof Cleaner) { ((Cleaner)r).clean(); continue; } ReferenceQueue q = r.queue; if (q != ReferenceQueue.NULL) q.enqueue(r); } } }
现在我们应该已经清楚了,守护线程一直执行入队操作,将key关联的Entry<K,V>放入queue中。
但是将key放入queue中需要前提条件: pending
这个pending是在垃圾回收的时候,JVM计算对象key的可达性后,发现没有该key对象的引用,那么就会把该对象关联的Entry<K,V>添加到pending中,
所以每次垃圾回收时发现弱引用对象没有被引用时,就会将该对象放入待清除队列中,最后由应用程序来完成清除,WeakHashMap中就负责由
方法expungeStaleEntries()来完成清除。
例子:
@Test public void weakHashMap(){ Map<String, String> weak = new WeakHashMap<String, String>(); weak.put(new String("1"), "1"); weak.put(new String("2"), "2"); weak.put(new String("3"), "3"); weak.put(new String("4"), "4"); weak.put(new String("5"), "5"); weak.put(new String("6"), "6"); System.out.println(weak.size()); System.gc(); //手动触发 Full GC try { Thread.sleep(50); //我的测试中发现必须sleep一下才能看到不一样的结果 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(weak.size()); }
上面的例子是可以正确的,但是下面的就有问题了:
@Test public void weakHashMap(){ Map<String, String> weak = new WeakHashMap<String, String>(); weak.put("1", "1"); weak.put("2", "2"); weak.put("3", "3"); weak.put("4", "4"); weak.put("5", "5"); weak.put("6", "6"); System.out.println(weak.size()); System.gc(); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(weak.size()); }
无论sleep多长时间,引用也不会被清除。这涉及到String在JVM中的工作方式了,这个问题留给读者自己思考。