Java集合框架:WeakHashMap

简介: WeakHashMap定义package java.util;import java.lang.ref.WeakReference;import java.lang.ref.

WeakHashMap定义

package java.util;
import java.lang.ref.WeakReference;
import java.lang.ref.ReferenceQueue;

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

  WeakHashMap实现了Map接口,是HashMap的一种实现,它比HashMap多了一个引用队列:

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

  博主认真比对过WeakHashMap和HashMap的源码,发现WeakHashMap中方法的实现方式基本和HashMap的一样,注意“基本”两个字,除了没有实现Cloneable和Serializable这两个标记接口,最大的区别在于在于expungeStaleEntries()这个方法,这个是整个WeakHashMap的精髓,我们稍后进行阐述。
  它使用弱引用作为内部数据的存储方案。WeakHashMap是弱引用的一种典型应用,它可以为简单的缓存表解决方案。


WeakHashMap使用

  我们举一个简单的例子来说明一下WeakHashMap的使用:

        Map<String,Integer> map = new WeakHashMap<>();
        map.put("s1", 1);
        map.put("s2", 2);
        map.put("s3", 3);
        map.put("s4", 4);
        map.put("s5", 5);
        map.put(null, 9);
        map.put("s6", 6);
        map.put("s7", 7);
        map.put("s8", 8);
        map.put(null, 11);
        for(Map.Entry<String,Integer> entry:map.entrySet())
        {
            System.out.println(entry.getKey()+":"+entry.getValue());
        }
        System.out.println(map);

  运行结果:

s4:4
s3:3
s6:6
null:11
s5:5
s8:8
s7:7
s1:1
s2:2
{s4=4, s3=3, s6=6, null=11, s5=5, s8=8, s7=7, s1=1, s2=2}

  WeakHashMap和HashMap一样key和value的值都可以为null,并且也是无序的。但是HashMap的null是存在table[0]中的,这是固定的,并且null的hash为0,而在WeakHashMap中的null却没有存入table[0]中。
  这是因为WeakHashMap对null值进行了包装:

 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;
    }

  当对map进行put和get操作的时候,将null值标记为NULL_KEY,然后对NULL_KEY即对new Object()进行与其他对象一视同仁的hash,这样就使得null和其他非null的值毫无区别。


JDK关键源码分析

  首先看一下Entry<K,V>这个静态内部类:

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

        /**
         * Creates new entry.
         */
        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;
        }
    //其余代码略
}

  可以看到Entry继承扩展了WeakReference类(有关Java的引用类型可以参考《Java引用类型》)。并在其构造函数中,构造了key的弱引用。
  此外,在WeakHashMap的各项操作中,比如get()、put()、size()都间接或者直接调用了expungeStaleEntries()方法,以清理持有弱引用的key的表象。
  expungeStaleEntries()方法的实现如下:

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;
                }
            }
        }
    }

  可以看到每调用一次expungeStaleEntries()方法,就会在引用队列中寻找是否有被清楚的key对象,如果有则在table中找到其值,并将value设置为null,next指针也设置为null,让GC去回收这些资源。


案例应用

  如果在一个普通的HashMap中存储一些比较大的值如下:

        Map<Integer,Object> map = new HashMap<>();
        for(int i=0;i<10000;i++)
        {
            Integer ii = new Integer(i);
            map.put(ii, new byte[i]);
        }

  运行参数:-Xmx5M
  运行结果:

     Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at collections.WeakHashMapTest.main(WeakHashMapTest.java:39)

  同样我们将HashMap换成WeakHashMap其余都不变:

         Map<Integer,Object> map = new WeakHashMap<>();
        for(int i=0;i<10000;i++)
        {
            Integer ii = new Integer(i);
            map.put(ii, new byte[i]);
        }

  运行结果:(无任何报错)
  这两段代码比较可以看到WeakHashMap的功效,如果在系统中需要一张很大的Map表,Map中的表项作为缓存只用,这也意味着即使没能从该Map中取得相应的数据,系统也可以通过候选方案获取这些数据。虽然这样会消耗更多的时间,但是不影响系统的正常运行。
  在这种场景下,使用WeakHashMap是最合适的。因为WeakHashMap会在系统内存范围内,保存所有表项,而一旦内存不够,在GC时,没有被引用的表项又会很快被清除掉,从而避免系统内存溢出。
  我们这里稍微改变一下上面的代码(加了一个List):

Map<Integer,Object> map = new WeakHashMap<>();
        List<Integer> list = new ArrayList<>();
        for(int i=0;i<10000;i++)
        {
            Integer ii = new Integer(i);
            list.add(ii);
            map.put(ii, new byte[i]);
        }   

  运行结果:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at collections.WeakHashMapTest.main(WeakHashMapTest.java:43)

  如果存放在WeakHashMap中的key都存在强引用,那么WeakHashMap就会退化成HashMap。如果在系统中希望通过WeakHashMap自动清楚数据,请尽量不要在系统的其他地方强引用WeakHashMap的key,否则,这些key就不会被回收,WeakHashMap也就无法正常释放它们所占用的表项

博主如是说:要想WeakHashMap能够释放掉key被GC的value的对象,尽可能的多调用下put/size/get等操作,因为这些方法会调用expungeStaleEntries方法,expungeStaleEntries方法是关键,而如果不操作WeakHashMap,以企图WeakHashMap“自动”释放内存是不可取的,这里的“自动”是指譬如map.put(obj,new byte[10M]);之后obj=null了,之后再也没掉用过map的任何方法,那么new出来的10M空间是不会释放的。


疑问

  楼主对于WeakHashMap一直有一个疑问,是这样的:
  我们知道WeakHashMap的key可以为null,那么当put一个key为null,value为一个很大对象的时候,这个很大的对象怎么采用WeakHashMap的自带的功能自动释放呢?
  代码如下:

Map<Object,Object> map = new WeakHashMap<>();
        map.put(null,new byte[5*1024*928]);
        int i = 1;
        while(true)
        {
            System.out.println();
            TimeUnit.SECONDS.sleep(2);
            System.out.println(map.size());
            System.gc();
            System.out.println("==================第"+i+++"次GC结束====================");
        }

  运行参数:-Xmx5M -XX:+PrintGCDetails
  运行结果:

1
[GC [PSYoungGen: 680K->504K(2560K)] 5320K->5240K(7680K), 0.0035741 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 504K->403K(2560K)] [ParOldGen: 4736K->4719K(5120K)] 5240K->5123K(7680K) [PSPermGen: 2518K->2517K(21504K)], 0.0254473 secs] [Times: user=0.06 sys=0.00, real=0.03 secs] 
==================第1次GC结束====================

1
[Full GC [PSYoungGen: 526K->0K(2560K)] [ParOldGen: 4719K->5112K(5120K)] 5246K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0172785 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
==================第2次GC结束====================

1
[Full GC [PSYoungGen: 41K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5153K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0178421 secs] [Times: user=0.03 sys=0.00, real=0.02 secs] 
==================第3次GC结束====================

1
[Full GC [PSYoungGen: 41K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5153K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0164874 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
==================第4次GC结束====================

1
[Full GC [PSYoungGen: 41K->0K(2560K)] [ParOldGen: 5112K->5112K(5120K)] 5153K->5112K(7680K) [PSPermGen: 2520K->2520K(21504K)], 0.0191096 secs] [Times: user=0.05 sys=0.00, real=0.02 secs] 
==================第5次GC结束====================
(一直循环下去)

  可以看到在map.put(null,new byte[5*1024*928]);之后,相应的内存一直没有得到释放
  通过显式的调用map.remove(null)可以将内存释放掉(如下代码所示)

    Map<Integer,Object> map = new WeakHashMap<>();
        System.gc();
        System.out.println("===========gc:1=============");
        map.put(null,new byte[4*1024*1024]);
        TimeUnit.SECONDS.sleep(5);
        System.gc();
        System.out.println("===========gc:2=============");
        TimeUnit.SECONDS.sleep(5);
        System.gc();
        System.out.println("===========gc:3=============");
        map.remove(null);
        TimeUnit.SECONDS.sleep(5);
        System.gc();
        System.out.println("===========gc:4=============");

  运行参数:-Xmx5M -XX:+PrintGCDetails
  运行结果:

[GC [PSYoungGen: 720K->504K(2560K)] 720K->544K(6144K), 0.0023652 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 504K->0K(2560K)] [ParOldGen: 40K->480K(3584K)] 544K->480K(6144K) [PSPermGen: 2486K->2485K(21504K)], 0.0198023 secs] [Times: user=0.11 sys=0.00, real=0.02 secs] 
===========gc:1=============
[GC [PSYoungGen: 123K->32K(2560K)] 4699K->4608K(7680K), 0.0026722 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 4576K->4578K(5120K)] 4608K->4578K(7680K) [PSPermGen: 2519K->2519K(21504K)], 0.0145734 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
===========gc:2=============
[GC [PSYoungGen: 40K->32K(2560K)] 4619K->4610K(7680K), 0.0013068 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 4578K->4568K(5120K)] 4610K->4568K(7680K) [PSPermGen: 2519K->2519K(21504K)], 0.0189642 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] 
===========gc:3=============
[GC [PSYoungGen: 40K->32K(2560K)] 4609K->4600K(7680K), 0.0011742 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 4568K->472K(5120K)] 4600K->472K(7680K) [PSPermGen: 2519K->2519K(21504K)], 0.0175907 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
===========gc:4=============
Heap
 PSYoungGen      total 2560K, used 82K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd14820,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 5120K, used 472K [0x00000000ff800000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 5120K, 9% used [0x00000000ff800000,0x00000000ff876128,0x00000000ffd00000)
 PSPermGen       total 21504K, used 2526K [0x00000000fa600000, 0x00000000fbb00000, 0x00000000ff800000)
  object space 21504K, 11% used [0x00000000fa600000,0x00000000fa8778f8,0x00000000fbb00000)

  如果真是只有通过remove的方式去删除null的键所指向的value的话,博主建议在使用WeakHashMap的时候尽量避免使用null作为键。如果有大神可以解答一下这个问题,请在下方留言。


参考资料:
1. 《Java引用类型
2. 《Java程序性能优化——让你的Java程序更快、更稳定》葛一鸣 等编著。

目录
相关文章
|
1月前
|
Java 数据库
在Java中使用Seata框架实现分布式事务的详细步骤
通过以上步骤,利用 Seata 框架可以实现较为简单的分布式事务处理。在实际应用中,还需要根据具体业务需求进行更详细的配置和处理。同时,要注意处理各种异常情况,以确保分布式事务的正确执行。
|
1月前
|
消息中间件 Java Kafka
在Java中实现分布式事务的常用框架和方法
总之,选择合适的分布式事务框架和方法需要综合考虑业务需求、性能、复杂度等因素。不同的框架和方法都有其特点和适用场景,需要根据具体情况进行评估和选择。同时,随着技术的不断发展,分布式事务的解决方案也在不断更新和完善,以更好地满足业务的需求。你还可以进一步深入研究和了解这些框架和方法,以便在实际应用中更好地实现分布式事务管理。
|
1月前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
148 3
|
13天前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
33 5
|
26天前
|
存储 缓存 安全
Java 集合框架优化:从基础到高级应用
《Java集合框架优化:从基础到高级应用》深入解析Java集合框架的核心原理与优化技巧,涵盖列表、集合、映射等常用数据结构,结合实际案例,指导开发者高效使用和优化Java集合。
37 4
|
1月前
|
人工智能 前端开发 Java
基于开源框架Spring AI Alibaba快速构建Java应用
本文旨在帮助开发者快速掌握并应用 Spring AI Alibaba,提升基于 Java 的大模型应用开发效率和安全性。
218 12
基于开源框架Spring AI Alibaba快速构建Java应用
|
1月前
|
消息中间件 Java 数据库连接
Java 反射最全详解 ,框架设计必掌握!
本文详细解析Java反射机制,包括反射的概念、用途、实现原理及应用场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Java 反射最全详解 ,框架设计必掌握!
|
1月前
|
开发框架 Java 关系型数据库
Java哪个框架适合开发API接口?
在快速发展的软件开发领域,API接口连接了不同的系统和服务。Java作为成熟的编程语言,其生态系统中出现了许多API开发框架。Magic-API因其独特优势和强大功能,成为Java开发者优选的API开发框架。本文将从核心优势、实际应用价值及未来展望等方面,深入探讨Magic-API为何值得选择。
51 2
|
1月前
|
前端开发 Java 数据库连接
你不可不知道的JAVA EE 框架有哪些?
本文介绍了框架的基本概念及其在编程领域的应用,强调了软件框架作为通用、可复用的软件环境的重要性。文章分析了早期Java EE开发中使用JSP+Servlet技术的弊端,包括可维护性差和代码重用性低等问题,并阐述了使用框架的优势,如提高开发效率、增强代码规范性和可维护性及提升软件性能。最后,文中详细描述了几种主流的Java EE框架,包括Spring、Spring MVC、MyBatis、Hibernate和Struts 2,这些框架通过提供强大的功能和支持,显著提升了Java EE应用的开发效率和稳定性。
75 1
|
1月前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
33 2