2023-01-14  阅读(2)
原文作者: HelloWorld_EE 原文地址:https://blog.csdn.net/u010412719/category_6159934_2.html

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:往一个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]);
                }
            }
        }

第二段代码,就是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]);
                }
            }
        }

运行上面的两段代码,发现,第一段代码是不能正常工作的,会抛“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);
                }
            }
        }

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

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

而如下的代码确能够正常工作,这两段代码的区别在于下面这段代码中调用了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());
                    }
    
                }
            }
        }

可能有人要问了,不是说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);
            }
        }

上面的代码运行时没有任何问题的,这也就证明了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()方法之后就不会报异常能正常工作的原因。

参考资料

1、http://hongjiang.info/java-referencequeue/:写的更详细


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文