zoukankan      html  css  js  c++  java
  • 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     }
    View Code

    通过查看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日,上午。

  • 相关阅读:
    docker如何将运行中的容器保存为docker镜像?
    java8流的地址
    maven命令package、install、deploy
    windows下设置redis开机自启动
    mysql的安装参考
    service mysql启动失败unit not found
    JAVA中Wait()与Notity()、同步队列与等待队列
    Java8函数式编程
    Groovy ConfigSlurper()读取配置文件简易示例
    SoapUI官方文档
  • 原文地址:https://www.cnblogs.com/developer_chan/p/11456093.html
Copyright © 2011-2022 走看看