首页 > 代码库 > 深入WeakHashMap

深入WeakHashMap

WeakHashMap

       最近工作中碰到了java.util.WeakHashMap<KV>,不解其中奥妙,遂查个究竟,顺带记录下来

       

Java引用类型

首先需要了解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 进行一些后期的回收工作。    

WeakHashMap中怎么实现Weak?

大家都知道MapEntry存储数据,看看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);


下面是WeakHashMapEntry实现,继承了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;但是弱引用的实现细节还是不清楚,接着扒。。。

Key如何清理

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)线程去进行GCConcurrentMarkSweepThread线程被创建的同时会创建一个SurrogateLockerThread(简称SLT)线程并且启动它,SLT启动之后,处于等待阶段。CMST开始GC时,会发一个消息给SLT让它去获取JavaReference对象的全局锁:lock。直到CMS GC完毕之后,JVM会将WeakHashMap中所有被回收的对象所属的WeakReference容器对象放入到Referencepending 属性当中(每次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);
         }
     }
 }



上面部分讲了JVMGC的时候帮我们把WeakHashMap中的key的内存释放掉了,那么 WeakHashMapEntry数据怎么释放,看看 WeakHashMap ReferenceQueue怎么起的作用? 

Entry如何清理

GC之后,WeakHashMap对象里面getput数据或者调用size方法的时候,WeakHashMapHashMap多了一个 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对应的EntryWeakHashMap中移除了。  

最后

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的策略