Java集合框架之八----------WeakHashMap源码解析

1.技术点

1.1 java中的引用

引用类型主要分为4种:①强引用;②软引用;③弱引用;④虚引用。强引用就是永远不会回收掉被引用的对象,比如说我们代码中new出来的对象。软引用表示有用但是非必需的,如果系统内存资源紧张,可能就会被回收;弱引用表示非必需的对象,只能存活到下一次垃圾回收发生之前;虚引用是最弱的,这个引用无法操作对象。

1.2引用队列ReferenceQueue

    Object o1 = new Object();

Integer o2 = new Integer((int) o1);

如果o1对象除了在o2中有引用之外没有别的地方存在引用,那么就可以回收o1。然后当这个o1被回收之后,我们就需要把o2放入引用队列中,所以引用队列(ReferenceQueue)就是Reference的监听器。在WeakHashMap中就是通过ReferenceQueue来反向处理map中的数据,如果对象被回收了,那么就需要把map中的对应数据移除。

 

public class ReferenceTest {

    private static final int _1MB = 1024*1024;//设置大小为1MB

 

    public static void main(String[] args) throws InterruptedException {

        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();//用引用队列进行监控引用的回收情况

        Object value = new Object();

        Map<Object, Object> map = new HashMap<Object, Object>();

        for (int i = 0; i < 100; i++) {//循环100次把数据插入到弱引用中(WeakReference), 同时把弱引用作为key存入HashMap

            byte[] bytes = new byte[_1MB];

            //每个弱引用中都有关联引用队列(referenceQueue),由于bytes只被weakReference引用,因此每次weakReference中的bytes被回收之后,那么这个weakReference对象就会放入引用队列

            WeakReference<byte[]> weakReference = new WeakReference<byte[]>(bytes, referenceQueue);

            //通过map存储弱引用对象

            map.put(weakReference, value);

        }

 

        Thread thread = new Thread(new Runnable() {//线程通过调用引用队列的情况查看那些对象被回收

            @SuppressWarnings("unchecked")

            public void run() {

                try {

                    int cnt = 0;

                    WeakReference<byte[]> k;

                    while ((k = (WeakReference<byte[]>) referenceQueue.remove()) != null) {//返回被回收对象的引用(注意本例中被回收的是bytes

                        System.out.println((cnt++)+"回收了"+k);

                        System.out.println("mapsize = " + map.size());//用于监控map的存储数量有没有发生变化

                    }

                } catch (Exception e) {

                    // TODO: handle exception

                }

            }

        });

 

        thread.start();

    }

}

流程:

①bytes对象存入weakReference对象中

②weakReference作为key存入HashMap中

③GC回收了bytes对象,这时,引用这个对象的weakReference对象就会被存储到referenceQueue中

④循环输出referenceQueue,输出被回收的bytes关联的weakReference对象

 

下面用WeakHashMap做测试

import java.util.Map;

import java.util.WeakHashMap;

 

public class ReferenceTest2 {

    private static final int _1MB = 1024*1024;//设置大小为1MB

 

    public static void main(String[] args) throws InterruptedException {

        Object value = new Object();

        Map<Object, Object> map = new WeakHashMap<Object, Object>();

        for (int i = 0; i < 100; i++) {//循环100次把数据插入WeakHashMap

            byte[] bytes = new byte[_1MB];

            map.put(bytes, value);

        }

        while (true) {//死循环监控map大小变化

            Thread.sleep(500);//稍稍停顿,效果更直观

            System.out.println(map.size());//打印WeakHashMap的大小

            System.gc();//建议系统进行GC

        }

    }

 

}

因为在插入过程中就存在了GC,会把WeakHashMap的内容清除,因为一旦GC发生,弱引用就会被清除,导致WeakHashMap大小为0;

在你触发一些操作的时候,WeakHashMap里面的数据就会被移除了

    public int size() {

        if (size == 0)

            return 0;

        expungeStaleEntries();从表中清除掉不存在的引用的方法。

        return size;

    }

2.源码分析

①引用队列

private final ReferenceQueue<Object> queue = new ReferenceQueue<>();

②静态内部类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;

        }

}

如果上面第一个代码所示,key值和referenceQueue作为weakReference的参数封装进了WeakReference中,GC的时候回收掉弱引用引用的对象,如上面的bytes,

核心移除mapKV方法

解释一下为什么对象回收之后,map中对应的KV也会被移除

private void expungeStaleEntries() {

GC之后,queue里面就会存着对应的reference的子类,这里是WeakReference的子类Entry。,

        for (Object x; (x = queue.poll()) != null; ) {

            synchronized (queue) {//线程同步,锁定队列

                @SuppressWarnings("unchecked")

                    Entry<K,V> e = (Entry<K,V>) x;

得到对应元素在table中的下标

                int i = indexFor(e.hash, table.length);

prev指向table[i]即链表头结点

                Entry<K,V> prev = table[i];

                Entry<K,V> p = prev;用来表示p的上一个节点

                while (p != null) {如果p节点存在

                    Entry<K,V> next = p.next;定义next执行pnext

                    if (p == e) {如果当前队列的节点e==p

                        if (prev == e)如果第一次prev就是e,说明e就是头结点,直接指向next

                            table[i] = next;

                        else否则prevnext指向pnext节点

                            prev.next = next;

                        // Must not null out e.next;

                        // stale entries may be in use by a HashIterator

                        e.value = null; // Help GC--evalue值置空

                        size--;

                        break;

                    }

不相等,就继续沿着链表往下找

                    prev = p;

                    p = next;

                }

            }

        }

}

通过hash值与length-1找到对应元素在table中的下标

private static int indexFor(int h, int length) {

        return h & (length-1);

    }

那在什么时候会调用这个expungeStaleEntries方法

//size方法

    public int size() {

        if (size == 0)

            return 0;

        expungeStaleEntries();//去除被回收的对象

        return size;

    }

 

//getTable方法(这个方法是putget方法的辅助方法)

   

    private Entry<K,V>[] getTable() {

        expungeStaleEntries();//去除被回收的对象

        return table;

    }

 

//resize扩容方法

    void resize(int newCapacity) {

        Entry<K,V>[] oldTable = getTable();

        int oldCapacity = oldTable.length;

        if (oldCapacity == MAXIMUM_CAPACITY) {

            threshold = Integer.MAX_VALUE;

            return;

        }

 

        Entry<K,V>[] newTable = newTable(newCapacity);

        transfer(oldTable, newTable);

        table = newTable;

        if (size >= threshold / 2) {

            threshold = (int)(newCapacity * loadFactor);

        } else {

            expungeStaleEntries();//去除被回收的对象

            transfer(newTable, oldTable);

            table = oldTable;

        }

    }

 

//get方法(基于上面说的getTable方法)

    public V get(Object key) {

        Object k = maskNull(key);

        int h = hash(k);

        Entry<K,V>[] tab = getTable();//getTable中包装了expungeStaleEntries方法

        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;

    }

 

//put方法(基于上面说的getTable方法)

    public V put(K key, V value) {

        Object k = maskNull(key);

        int h = hash(k);

        Entry<K,V>[] tab = getTable();//getTable中包装了expungeStaleEntries方法

        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;

            }

        }

WeekHashMap 的这个特点特别适用于需要缓存的场景,在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,不过并不是说这个用于缓存一定合理,因为如果你只put了,没有做其他get或者会触发expungeStaleEntries操作的方法, 那么该值就会一直存在了。建议还是使用LRU缓存会比较好

猜你喜欢

转载自blog.csdn.net/huangwei18351/article/details/82217704
今日推荐