搞定 WeakHashMap 的工作原理一篇文章就够了!!!

Posted 南淮北安

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了搞定 WeakHashMap 的工作原理一篇文章就够了!!!相关的知识,希望对你有一定的参考价值。

一、什么是 WeakHashMap?

从名字可以得知主要和Map有关,不过还有一个Weak,我们就更能自然而然的想到这里面还牵扯到一种弱引用结构,因此想要彻底搞懂,我们还需要知道四种引用。

强引用:
如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。
比如String str = "hello"这时候str就是一个强引用。

软引用:
内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。

弱引用:
如果一个对象具有弱引用,在垃圾回收时候,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。

虚引用:
如果一个对象具有虚引用,就相当于没有引用,在任何时候都有可能被回收。
使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。

WeakHashMap是基于弱引用的,也就是说只要垃圾回收机制一开启,就直接开始了扫荡,看见了就清除

二、为什么需要 WeakHashMap?

WeakHashMap正是由于使用的是弱引用,因此它的对象可能被随时回收。

更直观的说,当使用 WeakHashMap 时,即使没有删除任何元素,它的尺寸、get方法也可能不一样。

比如:

(1)调用两次size()方法返回不同的值;第一次为10,第二次就为8了。

(2)两次调用isEmpty()方法,第一次返回false,第二次返回true;

(3)两次调用containsKey()方法,第一次返回true,第二次返回false;

(4)两次调用get()方法,第一次返回一个value,第二次返回null;

是不是觉得有点恶心,这种飘忽不定的东西好像没什么用,试想一下,你准备使用WeakHashMap保存一些数据,写着写着都没了,那还保存个啥呀。

不过有一种场景,最喜欢这种飘忽不定、一言不合就删除的东西。

那就是缓存,在缓存场景下,由于内存是有限的,不能缓存所有对象,因此就需要一定的删除机制,淘汰掉一些对象

现在我们已经知道了WeakHashMap是基于弱引用,其对象可能随时被回收,适用于缓存的场景。

三、WeakHashMap 的例子

public class TestWeakHashMap {

    public static void main(String[] args) {
        WeakHashMap<String, String> weakHashMap = new WeakHashMap<>(10);

        String key0 = new String("str1");
        String key1 = new String("str2");
        String key2 = new String("str3");

        // 存放元素
        weakHashMap.put(key0, "data1");
        weakHashMap.put(key1, "data2");
        weakHashMap.put(key2, "data3");
        System.out.printf("weakHashMap: %s\\n", weakHashMap);

        // 是否包含某key
        System.out.printf("contains key str1 : %s\\n", weakHashMap.containsKey(key0));
        System.out.printf("contains key str2 : %s\\n", weakHashMap.containsKey(key1));

        // 移除key
        weakHashMap.remove(key0);
        System.out.printf("weakHashMap after remove: %s", weakHashMap);

        // 这意味着"弱键"key1再没有被其它对象引用,调用gc时会回收WeakHashMap中与key1对应的键值对
        key1 = null;
        // 内存回收,这里会回收WeakHashMap中与"key0"对应的键值对
        System.gc();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 遍历WeakHashMap
        for (Map.Entry<String, String> m : weakHashMap.entrySet()) {
            System.out.printf("next : %s >>> %s\\n", m.getKey(), m.getValue());
        }
        // 打印WeakHashMap的实际大小
        System.out.printf("after gc WeakHashMap size: %s\\n", weakHashMap.size());
    }
}


上面的例子展示了WeakHashMap的增删改查,以及弱键的回收,可以看到把Key的引用置为null,
gc后,会将该键值对回收。

四、WeakHashMap 的使用场景

一般用做缓存,比如Tomcat的源码里,实现缓存时会用到WeakHashMap,
在缓存系统中,使用WeakHashMap可以避免内存泄漏,但是使用WeakHashMap做缓存时要注意,如果只有它的key只有WeakHashMap本身在用,而在WeakHashMap之外没有对该key的强引用,那么GC时会回收这个key对应的entry。

所以WeakHashMap不能用做主缓存,合适的用法应该是用它做二级的内存缓存,即那么过期缓存数据或者低频缓存数据

public final class ConcurrentCache<K,V> {
 
    private final int size;
 
    private final Map<K,V> eden;
 
    private final Map<K,V> longterm;
 
    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }
 
    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            synchronized (longterm) {
                v = this.longterm.get(k);
            }
            if (v != null) {
                this.eden.put(k, v);
            }
        }
        return v;
    }
 
    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            synchronized (longterm) {
                this.longterm.putAll(this.eden);
            }
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}

源码中有eden和longterm的两个map,对jvm堆区有所了解的话,可以猜测出tomcat在这里是使用ConcurrentHashMap和WeakHashMap做了分代的缓存。

在put方法里,在插入一个k-v时,先检查eden缓存的容量是不是超了。

没有超就直接放入eden缓存,如果超了则锁定longterm将eden中所有的k-v都放入longterm。

再将eden清空并插入k-v。

在get方法中,也是优先从eden中找对应的v,如果没有则进入longterm缓存中查找,找到后就加入eden缓存并返回。

经过这样的设计,相对常用的对象都能在eden缓存中找到,不常用(有可能被销毁的对象)的则进入longterm缓存。

而longterm的key的实际对象没有其他引用指向它时,gc就会自动回收heap中该弱引用指向的实际对象,弱引用进入引用队列。

longterm调用expungeStaleEntries()方法,遍历引用队列中的弱引用,并清除对应的Entry,不会造成内存空间的浪费。

五、WeakHashMap 的数据结构

1. 类的定义

public class WeakHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V> {
}

2. 常量与变量

    private static final int DEFAULT_INITIAL_CAPACITY = 16;
 
    private static final int MAXIMUM_CAPACITY = 1 << 30;
 
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
    Entry<K,V>[] table;
 
    private int size;
 
    private int threshold;
 
    private final float loadFactor;
 
    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
 
    int modCount;
  • DEFAULT_INITIAL_CAPACITY : 初始容量
  • MAXIMUM_CAPACITY : 最大容量
  • DEFAULT_LOAD_FACTOR : 默认加载因子
  • table : Entry数组
  • size : 实际存放的数据个数
  • threshold : 扩容阈值
  • loadFactor : 加载因子
  • queue : 引用队列
  • modCount : 修改次数

3. Entry 类

   private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;
 
        Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
 
        @SuppressWarnings("unchecked")
        public K getKey() {
            return (K) WeakHashMap.unmaskNull(get());
        }
 
        public V getValue() {
            return value;
        }
 
        public V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        ...
    }

可以看到 Entry 类实现了 Map.Entry 接口,继承弱引用 (WeakReference),属性有 key,value 和 next 引用。

构造器中需要传入一个引用队列,方法主要看getKey():

return (K) WeakHashMap.unmaskNull(get());

再来看看 WeakHashMap 的静态方法 unmaskNull():

    private static final Object NULL_KEY = new Object();
    static Object unmaskNull(Object key) {
        return (key == NULL_KEY) ? null : key;
    }

判断key是否等于NULL_KEY来选择是否返回null。

4. 类关系图

六、WeakHashMap 的弱键回收

上面看完 WeakHashMap 的数据结构,那么 WeakHashMap 是如何实现弱键回收的呢?

其实根据前面的文章也能猜到,利用Reference和ReferenceQueue。

1. put 数据

    public V put(K key, V value) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] 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, value, queue, h, e);
        if (++size >= threshold)
            resize(tab.length * 2);
        return null;
    }
 
   private static Object maskNull(Object key) {
        return (key == null) ? NULL_KEY : key;
    }

判断key是否为null,为null的话将key赋值为NULL_KEY。
计算key的hash值,然后根据hash值查找待插入的位置。
遍历Entry数组,看该键是否已存在,存在的话则替换旧值,并返回旧值。
不存在则构建Entry对象存入数组。

这个流程和HashMap,HashTable等差不多。

2. get 数据

    public V get(Object key) {
        Object k = maskNull(key);
        int h = hash(k);
        Entry<K,V>[] tab = getTable();
        int index = indexFor(h, tab.length);
        Entry<K,V> e = tab[index];
        while (e != null) {
            if (e.hash == h && eq(k, e.get()))
                return e.value;
            e = e.next;
        }
        return null;
    }

一句话,先根据key的hash值找到索引位置,然后拿到Entry对象,再判断该Entry是否存在下一个引用(即hash碰撞),遍历该单链表,比较value值。

3. 弱键如何回收?

根据上面的分析就很容易得出了,WeakHashMap内部的数据存储是用Entry[]数组,即键值对数组。

Entry类继承于WeakReferece(弱引用),

弱引用的特点再重申下:当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

在构造一个Entry对象的时候,会传入一个ReferenceQueue,key为弱引用包裹的对象:

    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }

再来看看WeakHashMap如何清理已经被回收的key的,被回收的key会存放在引用队列中:

private void expungeStaleEntries() {
        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);
 
                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;
                        // Must not null out e.next;
                        // stale entries may be in use by a HashIterator
                        e.value = null; // Help GC
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

遍历引用队列,然后删除已被回收的键值对(从数组移除,改变单链表结点引用,将value赋值为null),该方法会在WeakHashMap增删改查、扩容的地方调用。

因为一个key-value存放到WeakHashMap中后,key会被用弱引用包起来存储,如果这个key在WeakHashMap外部没有强引用的话,GC时将被回收,然后WeakHashMap根据引用队列对已回收的key做清理。

参考:

【1】https://baijiahao.baidu.com/s?id=1666368292461068600&wfr=spider&for=pc
【2】https://blog.csdn.net/u014294681/article/details/86522487

以上是关于搞定 WeakHashMap 的工作原理一篇文章就够了!!!的主要内容,如果未能解决你的问题,请参考以下文章

弱键集合WeakHashMap的原理以及ConcurrentCache的原理

#yyds干货盘点# Map - WeakHashMap源码解析

WeakHashMap垃圾回收原理

容器--WeakHashMap

从原理上搞定编码-- Base64编码

面试必问 | 一文轻松搞定MapReduceHQL执行原理