Java之WeakHashMap源码浅析

 更新时间:2023年09月07日 09:50:57   作者:澄风  
这篇文章主要介绍了Java之WeakHashMap源码浅析,WeakHashMap从名字可以得知主要和Map有关,不过还有一个Weak,我们就更能自然而然的想到这里面还牵扯到一种弱引用结构,因此想要彻底搞懂,我们还需要知道四种引用,需要的朋友可以参考下

定义

从名字可以得知主要和Map有关,不过还有一个Weak,我们就更能自然而然的想到这里面还牵扯到一种弱引用结构,因此想要彻底搞懂,我们还需要知道四种引用。

  • 强引用:
    • 如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。 比如String str = "hello"这时候str就是一个强引用。
  • 软引用:
    • 内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。
  • 弱引用:
    • 如果一个对象具有弱引用,在垃圾回收时候,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。
  • 虚引用:
    • 如果一个对象具有虚引用,就相当于没有引用,在任何时候都有可能被回收。 使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。

源码解析

看下WeakHashMap 的构造函数

public WeakHashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Initial Capacity: "+
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load factor: "+
                                           loadFactor);
    int capacity = 1;
    // 保证容量是2的整数倍,有助于hash运算
    while (capacity < initialCapacity)
        capacity <<= 1;
    // 初始化table数组
    table = newTable(capacity);
    this.loadFactor = loadFactor;
    // 阀值
    threshold = (int)(capacity * loadFactor);
}

没什么好说的 table 是一个 Entry数组 Entry<K,V>[] table; newTable会初始化一个数组数组的容量就是前面计算出来的capacity,其值为2的整数次方。

HashMap的容量为什么是2的n次方?HashMap是如何保证容量是2的n次方的? HashMap容量取2的n次方,主要与hash寻址有关。在put(key,value)时,putVal()方法中通过i = (n - 1) & hash来计算key的散列地址。其实,i = (n - 1) & hash是一个%操作。也就是说,HashMap是通过%运算来获得key的散列地址的。但是,%运算的速度并没有&的操作速度快。而&操作能代替%运算,必须满足一定的条件,也就是a%b=a&(b-1)仅当b是2的n次方的时候方能成立。这也就是为什么HashMap的容量需要保持在2的n次方了。

再看下Entry的类定义

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final 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的,我们结合WeakReference再看一下:

public class WeakReference<T> extends Reference<T> {
    /**
     * Creates a new weak reference that refers to the given object.  The new
     * reference is not registered with any queue.
     * 创建一个新的弱应用给传入的对象,这个新的引用不注册任何队列
     *
     * @param referent object the new weak reference will refer to
     */
    public WeakReference(T referent) {
        super(referent);
    }
    /**
     * Creates a new weak reference that refers to the given object and is
     * registered with the given queue.
     * 创建一个新的弱应用给传入的对象,这个新的引用注册给一个给定的队列
     *
     * @param referent object the new weak reference will refer to
     * @param q the queue with which the reference is to be registered,
     *          or <tt>null</tt> if registration is not required
     */
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

我们发现在 weakhashmap 中把key注册给了 WeakReference ,也就是说在 WeakHashMap 中key是一个弱引用。但这个queue是什么我们接着看,在往 WeakHashMap 中put一个元素的时候,会创建Entry。再看 WeakHashMap 的put操作,我们如果熟悉 HashMap 其实我们不需要怎么看这部分的代码,无非是计算hash值,散列分布到数组的各个位置,如果 hash 冲突使用拉链法进行解决。这里和hashmap有一点不一样的是hashmap如果链长达到阀值会使用红黑树。

public V put(K key, V value) {
    // 如果key是null则给定一个空的对象进行修饰
    Object k = maskNull(key);
    // 计算key的hash
    int h = hash(k);
    // 获取table
    Entry<K,V>[] tab = getTable();
    // 根据hash找到数组下标
    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;
        }
    }
    modCount++;
    Entry<K,V> e = tab[i];
    tab[i] = new Entry<>(k, value, queue, h, e);
    if (++size >= threshold) // 是否达到阀值达到阀值就扩容
        resize(tab.length * 2);
    return null;
}

可以看到这个queue是一个实例化final修饰的属性。

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

再看下getTable是什么情况,看源码会知道所有的WeekHashMap的所有操作都要调用 getTable -> expungeStaleEntries

private Entry<K,V>[] getTable() {
    expungeStaleEntries();
    return table;
}

我们看下expungeStaleEntries做了哪些事情?

private void expungeStaleEntries() {
    // 从 ReferenceQueue中拉取元素
    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
                    // 拿到entry的值赋值为null帮助GC
                    e.value = null; // Help GC
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}

expungeStaleEntries 就是WeakHashMap的核心了,它承担着Map中死对象的清理工作。原理就是依赖WeakReference和ReferenceQueue的特性。

在每个WeakHashMap都有个ReferenceQueue queue,在Entry初始化的时候也会将queue传给WeakReference,这样当某个可以key失去所有强应用之后,其key对应的WeakReference对象会被放到queue里,有了queue就知道需要清理哪些Entry了。

这里也是整个WeakHashMap里唯一加了同步的地方。除了上文说的到resize中调用了expungeStaleEntries(),size()中也调用了这个清理方法。另外 getTable()也调了,这就意味着几乎所有其他方法都间接调用了清理。

WeakHashMap的一点点缺点

提到缺点我不太认为是缺点,在某种场景下缺点也有可能是优点,而且很多缺点也是可以弥补的。

但非要说个一二三,这里列出下面两种:

1.非线程安全

关键修改方法没有提供任何同步,多线程环境下肯定会导致数据不一致的情况,所以使用时需要多注意。

2.单纯作为Map没有HashMap好

HashMap在Jdk8做了好多优化,比如单链表在过长时会转化为红黑树,降低极端情况下的操作复杂度。但WeakHashMap没有相应的优化,有点像jdk8之前的HashMap版本。

WeakHashMap可以应用的地方

1.缓存

2.诊断工具,比如atlas,将字节码缓存放入到WeakHashMap中

到此这篇关于Java之WeakHashMap源码浅析的文章就介绍到这了,更多相关WeakHashMap源码内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringBoot中实现数据字典的示例代码

    SpringBoot中实现数据字典的示例代码

    这篇文章主要介绍了SpringBoot中实现数据字典的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • Java 获取本机的IP与MAC地址实现详解

    Java 获取本机的IP与MAC地址实现详解

    这篇文章主要介绍了Java 获取本机的IP与MAC地址实现详解的相关资料,需要的朋友可以参考下
    2016-09-09
  • 教你用java stream对集合中的对象按指定字段进行分组并统计

    教你用java stream对集合中的对象按指定字段进行分组并统计

    这篇文章主要给大家介绍了关于用java stream对集合中的对象按指定字段进行分组并统计的相关资料,本文主要介绍了如何利用Java的Stream流来实现在list集合中,对具有相同name属性的对象进行汇总计算的需求,需要的朋友可以参考下
    2024-10-10
  • Java properties 和 yml 的区别解析

    Java properties 和 yml 的区别解析

    properties和yml都是Spring Boot支持的两种配置文件,它们可以看做Spring Boot在不同时期的两种“产品”,这篇文章主要介绍了Java properties 和 yml 的区别,需要的朋友可以参考下
    2023-02-02
  • spring(java,js,html) 截图上传图片实例详解

    spring(java,js,html) 截图上传图片实例详解

    这篇文章主要介绍了spring(java,js,html) 截图上传图片实例详解的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-07-07
  • Spring Boot集成starrocks快速入门Demo(适用场景)

    Spring Boot集成starrocks快速入门Demo(适用场景)

    StarRocks 是新一代极速全场景 MPP (Massively Parallel Processing) 数据库,StarRocks 的愿景是能够让用户的数据分析变得更加简单和敏捷,这篇文章主要介绍了Spring Boot集成starrocks快速入门Demo,需要的朋友可以参考下
    2024-08-08
  • java和matlab画多边形闭合折线图示例讲解

    java和matlab画多边形闭合折线图示例讲解

    由于要将“哈密顿回路问题(TSP)”的求解中间结果表示出来,查了一下使用程序画多边形图形。现在在总结一下,这个图是“由给定节点首尾相连的”闭合多边形
    2014-02-02
  • 关于Mybatis 中使用Mysql存储过程的方法

    关于Mybatis 中使用Mysql存储过程的方法

    这篇文章给大家介绍了Mybatis 中使用Mysql存储过程的方法,本文通过实例代码相结合的形式给大家介绍的非常详细,具有参考借鉴价值,需要的朋友参考下吧
    2018-03-03
  • Java进程cpu频繁100%问题解决方案

    Java进程cpu频繁100%问题解决方案

    这篇文章主要介绍了Java进程cpu频繁100%问题解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-10-10
  • Netty分布式pipeline管道传播事件的逻辑总结分析

    Netty分布式pipeline管道传播事件的逻辑总结分析

    这篇文章主要为大家介绍了Netty分布式pipeline管道传播事件总结分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-03-03

最新评论