Java源码分析——java.util工具包解析(四)——四大引用类型以及WeakHashMap类解析

版权声明:博主GitHub地址https://github.com/suyeq欢迎大家前来交流学习 https://blog.csdn.net/hackersuye/article/details/84450581

    WeakHashMap是Map的一种很独特的实现,从它的名字可以看出,它是存贮弱引用的映射的,先来复习一下Java中的四大引用类型:

  1. 强引用:我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。强引用的对象垃圾回收器绝不会回收它。当内存空间不足,jvm宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
  2. 软引用:软引用是当jvm中内存不够的情况下会回收其对象,在内存充足的情况下与强引用别无二样。
  3. 弱引用:弱引用是只要GC扫描到了弱引用,那么它指向的对象就会不管内存是否充足都会进行回收。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,jvm就会把这个弱引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了弱引用,来了解被引用的对象是否将要被垃圾回收。
  4. 虚引用:虚引用与没有引用没有什么区别,相当于没有引用指向改对象。虚 引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。

    通过几个例子来加深对四大引用类型的理解,先看第一个例子:

		String reference="蕾姆";//reference就是一个强引用
        SoftReference<String> stringSoftReference=new SoftReference<>(reference);
        reference=null;
        System.gc();
        System.out.println(stringSoftReference.get());//输出:蕾姆

    上述"蕾姆"有两个引用指向它,一个是强引用reference,另外一个是软引用stringSoftReference。从代码中可以看出,即便调用了GC,将强引用置为null,也依旧没有回收其指向的对象。再看下一个,为了了解什么时候对象回收了,重写finalize方法:

class Rem{
    long[] l=new long[10000];
    @Override
    protected void finalize(){
        System.out.println("小蕾姆被回收了");
    }
    public String toString(){
        return "你好,蕾姆";
    }
}
public class Test {
    public static void main(String args[]) {
        WeakReference<Rem> weakReference=new WeakReference<>(new Rem());
        SoftReference softReference=new SoftReference(new Rem());
        System.gc();
        System.out.println(softReference.get());
    }
}

//输出:小蕾姆被回收了
//你好,蕾姆

    为了使得GC能找到需要回收的对象,在Rem类里定义了一个很大的数组,方便GC进行回收,从代码可以看出,弱引用指向的对象是被回收了的,而软引用则没有被回收掉。理解了上述代码后,再来看看WeakHashMap类,该类继承自AbstractMap抽象类,与HasnMap类相比,它多一个引用队列:

public class WeakHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V> {
    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<>();
}

    经过上述的引用类型的讨论,很容易的得出结论,queue 里存贮着是需要被GC回收掉的引用。同时需要注意的是HashMap类的键是null的话,从null得出的哈希值是0,所以会存贮在第一个桶中,而WeakHashMap类则不一样,它给键为null值定义了一个Object对象:

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

    因此,键为null时与其他的键就没什么区别了。其中它的节点定义是继承了WeakReference,也就是说它里面保存的所有节点都是弱引用:

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;
}

    在WeakHashMap的各项操作中,比如get()、put()、size()都间接或者直接调用了expungeStaleEntries()方法,以清理弱引用指向的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;
                        //将被清理的键对应的值置为null
                        e.value = null; 
                        //节点数减一
                        size--;
                        break;
                    }
                    prev = p;
                    p = next;
                }
            }
        }
    }

    那么,在什么场景下可以用到WeakHashMap类呢?可以再存贮很大的值的场景下使用到WeakHashMap类,比如存贮上万个节点时,利用HashMap存贮:

class Rem{
    int i;
    long[] l=new long[10000];
    public Rem(int i){
        this.i=i;
    }
    @Override
    protected void finalize(){
        System.out.println("小蕾姆被回收了");
    }
    public String toString(){
        return "你好,蕾姆";
    }
}
public class Test {
    public static void main(String args[]) {
        HashMap<Integer,Rem> map=new HashMap<>();
//        Map<Integer,Rem> map=new WeakHashMap<>();
        for (int i=0;i<10000;i++){
            map.put(i,new Rem(i));
        }
    }
}

    会报内存溢出的异常,而用WeakHashMap类来存贮则不会,但会一直显示对象被回收掉。从中我们可以得到WeakHashMap的应用场景,这两段代码比较可以看到WeakHashMap的功效,如果在系统中需要一张很大的Map表,Map中的表项作为缓存只用,这也意味着即使没能从该Map中取得相应的数据,系统也可以通过候选方案获取这些数据。虽然这样会消耗更多的时间,但是不影响系统的正常运行。在这种场景下,使用WeakHashMap是最合适的。因为WeakHashMap会在系统内存范围内,保存所有表项,而一旦内存不够,在GC时,没有被引用的表项又会很快被清除掉,从而避免系统内存溢出。

猜你喜欢

转载自blog.csdn.net/hackersuye/article/details/84450581