最近工作中碰到了java.util.WeakHashMap<K, V>,不解其中奥妙,遂查个究竟,顺带记录下来
首先需要了解Java四种引用类型:
强引用(StrongReference)
强引用是使用最普遍的引用,平时我们常写的A a = new A();就是强引用
GC不会回收强引用,即使内存不足的情况下也不会,宁可OOM
软引用(SoftReference)
SoftReference 的主要特点是具有较强的引用功能。
只有当内存不够的时候才进行回收,而在内存足够的时候,通常不被回收。
另外,引用对象还能保证在 Java 抛出 OutOfMemoryError 之前,被设置为null。
软引用的使用可以参考guava-cache
弱引用(WeakReference)
WeakReference 在垃圾回收器运行时,一定会被回收,而不像 SoftReference 需要条件。但是,若对象的引用关系复杂,则可能需要多次回收才能达到目的。
虚引用(PhantomReference)
PhantomReference 主 要 用 于 辅 助 finalize 方法。
PhantomReference 对象执行完了 finalize 方法后,成为 Unreachable Objects。
但还未被回收,在此时,可以辅助 finalize 进行一些后期的回收工作。
大家都知道Map用Entry存储数据,看看WeakHashMap实现细节
Put部分的代码如下:
Entry<K,V> e = tab[i]; tab[i] = new Entry<K,V>(k, value, queue, h, e); if (++size >= threshold) resize(tab.length * 2);
下面是WeakHashMap的Entry实现,继承了WeakReference
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; /** * 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; }
将Key处理成Reference:
Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; }
与HashMap比较一下,Entry不直接引用Key这个对象,而是将引用关系放到了父类WeakReference中,可以看出WeakHashMap将传入的key包装成了WeakReference,并传入了一个ReferenceQueue;但是弱引用的实现细节还是不清楚,接着扒。。。
Reference类中有一段static代码
static private class Lock { }; private static Lock lock = new Lock(); private static Reference pending = null; 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(); }
线程的优先级设成MAX,是一个什么样的线程需要如此高的权限?pending 、lock 都被static声明,lock.wait之后谁来唤醒,互联网上一顿搜罗,才明白JVM参与了这些事。
用通俗的话把JVM干的事串一下: 假设,WeakHashMap对象里面已经保存了很多对象的引用。JVM使用进行CMS GC的时候,会创建一个ConcurrentMarkSweepThread(简称CMST)线程去进行GC,ConcurrentMarkSweepThread线程被创建的同时会创建一个SurrogateLockerThread(简称SLT)线程并且启动它,SLT启动之后,处于等待阶段。CMST开始GC时,会发一个消息给SLT让它去获取Java层Reference对象的全局锁:lock。直到CMS GC完毕之后,JVM会将WeakHashMap中所有被回收的对象所属的WeakReference容器对象放入到Reference的pending 属性当中(每次GC完毕之后,pending属性基本上都不会为null了),然后通知SLT释放并且notify全局锁: lock。此时激活了ReferenceHandler线程的run方法,使其脱离wait状态,开始工作了。ReferenceHandler这个线程会将pending中的所有WeakReference对象都移动到它们各自的列队当中,比如当前这个WeakReference属于某个WeakHashMap对象,那么它就会被放入相应的ReferenceQueue列队里面(该列队是链表结构)。
想要了解具体细节,再深扒一下openjdk的源码instanceRefKlass.cpp获得lock部分
void instanceRefKlass::acquire_pending_list_lock(BasicLock *pending_list_basic_lock) { // we may enter this with pending exception set PRESERVE_EXCEPTION_MARK; // exceptions are never thrown, needed for TRAPS argument Handle h_lock(THREAD, java_lang_ref_Reference::pending_list_lock()); ObjectSynchronizer::fast_enter(h_lock, pending_list_basic_lock, false, THREAD); assert(ObjectSynchronizer::current_thread_holds_lock( JavaThread::current(), h_lock), "Locking should have succeeded"); if (HAS_PENDING_EXCEPTION) CLEAR_PENDING_EXCEPTION; }
Gc完成后, pending赋值,lock释放
void instanceRefKlass::release_and_notify_pending_list_lock( BasicLock *pending_list_basic_lock) { // we may enter this with pending exception set PRESERVE_EXCEPTION_MARK; // exceptions are never thrown, needed for TRAPS argument // Handle h_lock(THREAD, java_lang_ref_Reference::pending_list_lock()); assert(ObjectSynchronizer::current_thread_holds_lock( JavaThread::current(), h_lock), "Lock should be held"); // Notify waiters on pending lists lock if there is any reference. if (java_lang_ref_Reference::pending_list() != NULL) { ObjectSynchronizer::notifyall(h_lock, THREAD); } ObjectSynchronizer::fast_exit(h_lock(), pending_list_basic_lock, THREAD); if (HAS_PENDING_EXCEPTION) CLEAR_PENDING_EXCEPTION; }
lock释放后, ReferenceHandler线程进入正常运转,将 pending 中的Reference对象压入了各自的 ReferenceQueue中
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); } } }
上面部分讲了JVM在GC的时候帮我们把WeakHashMap中的key的内存释放掉了,那么 WeakHashMap中Entry数据怎么释放,看看 WeakHashMap的 ReferenceQueue怎么起的作用?
当GC之后,WeakHashMap对象里面get、put数据或者调用size方法的时候,WeakHashMap比HashMap多了一个 expungeStaleEntries()方法
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; } } }
expungeStaleEntries方法 就是将ReferenceQueue列队中的WeakReference依依poll出来去和Entry[]数据做比较,如果发现相同的,则说明这个Entry所保存的对象已经被GC掉了,那么将Entry[]内的Entry对象剔除掉,这样就把被GC掉的 WeakReference对应的Entry从WeakHashMap中移除了。
public static void main(String[] args) { Map<String, String> data = new WeakHashMap<String, String>(); data.put("123", "123"); data.put("124", "124"); data.put("125", "125"); data.put("126", "126"); System.gc(); data.size(); System.out.println(data); }
代码中显式的调用System.gc()之后,data中的数据被清空了吗?答案是没有
经过测试一次gc未必能完全回收所有的weakreference对象,weakreference也有可能出现在old区,至于为什么就要看GC的策略