《java源码分析系列》WeakHashMap和HashMap的区别

WeakHashMap和HashMap的区别

前面对HashMap的源码和WeakHashMap的源码分别进行了分析。在WeakHashMap源码分析博文中有对与HashMap区别的比较,但是不够具体系统。加上本人看了一些相关的博文,发现了一些好的例子来说明这两者的区别,因此,就有了这篇博文。

WeakHashMap和HashMap一样,WeakHashMap也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以为null。不过WeakHashMap的键是“弱键”(注:源码中Entry中的定义是这样的:private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>,即Entry实现了WeakReference类),当WeakHashMap某个键不再正常使用时,会被从WeakHashMap自动删除。更精确的说,对于一个给定的键,其映射的存在并不能阻止垃圾回收器对该键的丢弃,这就使该键称为被终止的,被终止,然后被回收,这样,这就可以认为该键值对应该被WeakHashMap删除。因此,WeakHashMap使用了弱引用作为内部数据的存储方案,,WeakHashMap可以作为简单缓存表的解决方案,当系统内存不足时,垃圾收集器会自动的清除没有在任何其他地方被引用的键值对。如果需要用一张很大的Map作为缓存表时,那么可以考虑使用WeakHashMap。

从源码的角度,我们来分析下上面这段话是如何来工作的??

在WeakHashMap实现中,借用了ReferenceQueue这个“监听器”来保存被GC回收的”弱键”,然后在每次使用WeakHashMap时,就在WeakHashMap中删除ReferenceQueue中保存的键值对。即WeakHashMap的实现是通过借用 
ReferenceQueue这个“监听器”来优雅的实现自动删除那些引用不可达的key的。关于ReferenceQueue会在下篇博文中进行介绍

具体如下:

WeakHashMap是通过数组table保存Entry(键值对);每个Entry实际上就是一个链表来实现的。当某“弱键”不再被其它对象引用,就会被GC回收时,这个“弱键”也同时被添加到ReferenceQueue队列中。当下一步我们需要操作WeakHashMap时,会先同步table、queue,table中保存了全部的键值对,而queue中保存的是GC回收的键值对;同步他们,就是删除table中被GC回收的键值对。

源码中完成“删除”操作的函数代码如下:

    /**
     * Expunges stale entries from the table.
     *翻译:删除过时的条目,即将ReferenceQueue队列中的对象引用全部在table中给删除掉
     *思路:如何删除一个table的节点e,方法为:首先计算e的hash值,接着根据hash值找到其在table的位置,然后遍历链表即可。
     */
    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;
                }
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

例子说明1:往一个WeakHashMap中添加大量的元素

上面说的可能比较空,比如为什么可以作为缓冲表呀之类,可能看一个实际例子之后我们就可以更好的理解上面的两段话

第一段代码,就是HashMap的应用,往HashMap中存放一系列很大的数据。

    public class TestHashMap {

        public static void main(String[] args){
            Map<Integer,byte[]> hashMap = new HashMap<Integer,byte[]>();
            for(int i=0;i<100000;i++){
                hashMap.put(i, new byte[i]);
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

第二段代码,就是WeakHashMap的应用,往WeakHashMap中存放与上例HashMap相同的数据。

    public class TestWeakHashMap {

        public static void main(String[] args){
            Map<Integer,byte[]> weakHashMap = new WeakHashMap<Integer,byte[]>();
            for(int i=0;i<100000;i++){
                weakHashMap.put(i, new byte[i]);
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

运行上面的两段代码,发现,第一段代码是不能正常工作的,会抛“java.lang.OutOfMemoryError: Java heap space”,而第二段代码就可以正常工作。

以上就说明了,WeakHashMap当系统内存不足时,垃圾收集器会自动的清除没有在任何其他地方被引用的键值对,因此可以作为简单缓存表的解决方案。而HashMap就没有上述功能。

但是,如果WeakHashMap的key在系统内持有强引用,那么WeakHashMap就退化为了HashMap,所有的表项都不会被垃圾回收器回收。

例子说明2:一系列的WeakHashMap,往每个WeakHashMap中只添加一个大的数据

看如下的例子,例子的代码是,在for循环中每次都new一个WeakHashMap对象,且每个对象实例中只添加一个key和value都是大的数组对象。看会出现上面现象???

    public class TestWeakHashMap3 {

        public static void main(String[] args){
            List<WeakHashMap<Integer[][], Integer[][]>> maps = new ArrayList<WeakHashMap<Integer[][],Integer[][]>>();   
            int totalNum = 10000;
            for(int i=0;i<totalNum;i++){
                WeakHashMap<Integer[][], Integer[][]> w = new WeakHashMap<Integer[][], Integer[][]>();
                w.put(new Integer[1000][1000], new Integer[1000][1000]);
                maps.add(w);
                System.gc();//显示gc
                System.out.println(i);
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

上面的运行结果如下:即由于空间不足报异常错误。

    /*
     * 运行结果:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
     *  at com.wrh.testhashmap.TestWeakHashMap3.main(TestWeakHashMap3.java:15)
     * */
  • 1
  • 2
  • 3
  • 4

而如下的代码确能够正常工作,这两段代码的区别在于下面这段代码中调用了WeakHashMap的size()方法。

    public class TestWeakHashMap5 {

        public static void main(String[] args){
            List<WeakHashMap<Integer[][], Integer[][]>> maps = new ArrayList<WeakHashMap<Integer[][],Integer[][]>>();   
            int totalNum = 10000;
            for(int i=0;i<totalNum;i++){
                WeakHashMap<Integer[][], Integer[][]> w = new WeakHashMap<Integer[][], Integer[][]>();
                w.put(new Integer[1000][1000], new Integer[1000][1000]);
                maps.add(w);
                System.gc();
                for(int j=0;j<i;j++){
                    System.out.println("第"+j+"个map的大小为:"+maps.get(j).size());
                }

            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

可能有人要问了,不是说WeakHashMap具有会自动进行垃圾回收,第一种情况为什么会报OOM异常了,第二种情况会正常工作呢????

首先要说明的是,第一段代码并不是没有执行GC,而是仅对WeakHashMap中的key中的Integer数组进行了回收,而value依然保持。我们先来看如下的例子:将value换成一个小的对象Object,就会证明这一点内容。

    public static void main(String[] args){
        List<WeakHashMap<Integer[][], Object>> maps = new ArrayList<WeakHashMap<Integer[][],Object>>(); 
        int totalNum = 10000;
        for(int i=0;i<totalNum;i++){
            WeakHashMap<Integer[][], Object> w = new WeakHashMap<Integer[][], Object>();
            w.put(new Integer[1000][1000], new Object());
            maps.add(w);
            System.gc();
            System.out.println(i);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

上面的代码运行时没有任何问题的,这也就证明了key中的Integer数组确实被回收了,那为何key中的reference数据被GC,却没有触发WeakHashMap去做清理整个key的操作呢??

原因是在于:在进行put操作后,虽然GC将WeakReference的key中的Integer数组回收了,并将事件通过到了ReferenceQueue,但是后续却没有相应的动作去触发WeakHashMap来进行处理ReferenceQueue,所以WeakReference包装的key依然存在在WeakHashMap中,其对应的value也就依然存在。

但是在WeakHashMap中会删除那些已经被GC的键值对在源码中是通过调用expungeStaleEntries函数来完成的,而这个函数只在WeakHashMap的put、get、size()等方法中才进行了调用。因此,只有put、get、size()方法来可以触发WeakHashMap来进行处理ReferenceQueue。

以上也就是为什么上面的第二段代码中调用下WeakHashMap的size()方法之后就不会报异常能正常工作的原因。

阅读更多
想对作者说点什么?
相关热词

博主推荐

换一批

没有更多推荐了,返回首页