WeakHashMap详解

1 概述

在 Java 编程中,Map 是一个非常常用的数据结构,其中 HashMap 是最常见的实现之一。然而,在某些高级场景中,我们可能会使用 WeakHashMapWeakHashMapHashMap 在大多数行为上相似,但有一个关键区别:WeakHashMap 不会阻止垃圾回收器(GC)回收其 key 对象。本文将深入探讨 WeakHashMap 是如何实现这一特性的。

2 Java中的引用类型

在开始WeakHashMap之前,我们先要对弱引用有一定的了解。

在 Java 中,引用类型有四种:

  • 强引用(Strong Reference):这是我们正常编码时默认的引用类型。如果一个对象到 GC Roots 有强引用可达,那么这个对象就不会被垃圾回收。
  • 软引用(Soft Reference):软引用阻止 GC 回收的能力相对较弱。如果一个对象只有软引用可达,那么它会在内存不足时被垃圾回收器回收。
  • 弱引用(WeakReference):弱引用无法阻止 GC 回收。如果一个对象只有弱引用可达,那么在下一个 GC 回收执行时,该对象就会被回收掉。
  • 虚引用(Phantom Reference):虚引用非常脆弱,它的唯一作用是当其指向的对象被回收之后,自己被加入到引用队列,用作记录该引用指向的对象已被销毁。

3 引用队列(Reference Queue)

引用队列是一个重要的概念。一般情况下,当一个对象被标记为垃圾(并不代表已经被回收)后,会加入到引用队列。对于虚引用来说,它指向的对象只有在被回收后才会加入引用队列,因此可以用作记录该引用指向的对象是否已被回收。

4 WeakHashMap 的实现

在Java 8中,WeakHashMapEntry 继承了 WeakReference,并将 key 作为 WeakReference 引用的对象。以下是 WeakHashMapEntry 类的简化实现:

java 复制代码
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;

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

    @SuppressWarnings("unchecked")
    public K getKey() {
        return (K) WeakHashMap.unmaskNull(get());
    }

    public V getValue() {
        return value;
    }

    public V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;
        K k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            V v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }

    public int hashCode() {
        K k = getKey();
        V v = getValue();
        return Objects.hashCode(k) ^ Objects.hashCode(v);
    }

    public String toString() {
        return getKey() + "=" + getValue();
    }
}

在这个实现中,key 被包装在 WeakReference 中,并且 WeakHashMap 还维护了一个ReferenceQueue,用于记录那些已经被垃圾回收的 key

因此WeakHashMap利用了WeakReference的机制来实现不阻止GC回收Key。

5 如何删除被回收的 key 数据

WeakHashMap 的 Javadoc 中提到,当 key 不再被引用时,其对应的 key/value 也会被移除。那么 WeakHashMap 是如何实现这一点的呢?

通常有两种假设策略:

  1. 当对象被回收时,进行通知:这种方式看似更高效,但 Java 中没有一个可靠的通知回调机制。
  2. WeakHashMap 轮询处理失效的 EntryWeakHashMap 采用的就是这种方式。在 putgetsize 等方法调用时,都会预先调用一个 expungeStaleEntries 方法,来检查并删除失效的 Entry

例如,在 put 方法中:

java 复制代码
public V put(K key, V value) {
    Object k = maskNull(key);
    int h = hash(k);
    Entry<K,V>[] tab = getTable();
    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;
}

put 方法中,WeakHashMap 会调用 getTable 方法,而 getTable 方法会调用 expungeStaleEntries 方法来清理失效的 Entry

java 复制代码
private Entry<K,V>[] getTable() {
    expungeStaleEntries();
    return table;
}

以下是 expungeStaleEntries 方法的实现:

java 复制代码
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;
            }
        }
    }
}

在这个方法中,WeakHashMap 会遍历 ReferenceQueue,找到那些已经被垃圾回收的 Entry,并从 table 中删除这些 Entry

6 为什么没有使用通知机制

在 Java 中,没有一个可靠的通知回调机制来通知对象被回收。虽然 finalize 方法可以作为辅助验证手段,但它并不是标准的,不同的 JVM 实现可能会有不同的行为,甚至可能不调用 finalize 方法。因此,WeakHashMap 选择了轮询 ReferenceQueue 的方式来处理被回收的 key

7 验证 WeakHashMap 的行为

为了验证 WeakHashMap 的行为,我们可以定义一个简单的 MyObject 类,并重写其 finalize 方法:

java 复制代码
class MyObject {
    private String id;

    public MyObject(String id) {
        this.id = id;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("Object(" + id + ") finalize method is called");
        super.finalize();
    }
}

然后编写测试代码:

java 复制代码
import java.util.WeakHashMap;

public class WeakHashMapTest {
    private static WeakHashMap<MyObject, Integer> weakHashMap = new WeakHashMap<>();
    private static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            weakHashMap.put(new MyObject(String.valueOf(count)), count);
            count++;
            System.out.println("WeakHashMap size: " + weakHashMap.size());
        }

        // 强制触发 GC
        System.gc();

        // 等待一段时间,确保 GC 完成
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 再次添加元素
        weakHashMap.put(new MyObject(String.valueOf(count)), count);
        count++;
        System.out.println("WeakHashMap size: " + weakHashMap.size());
    }
}

运行结果如下:

java 复制代码
WeakHashMap size: 1
WeakHashMap size: 2
WeakHashMap size: 3
Object(0) finalize method is called
Object(1) finalize method is called
Object(2) finalize method is called
WeakHashMap size: 1

在这个测试中,我们首先向 WeakHashMap 中添加了三个 Entry,然后强制触发 GC。在 GC 完成后,WeakHashMap 中的 key 被回收,WeakHashMap 的大小减少到 1。

8 总结

WeakHashMap 通过使用 WeakReferenceReferenceQueue 来实现不阻止 GC 回收 key 的功能。它通过轮询 ReferenceQueue 来检测并删除那些已经被垃圾回收的 key,而不是依赖于不可靠的通知机制。这种设计使得 WeakHashMap 在处理那些可能被垃圾回收的 key 时非常有效。

9 思维导图

参考链接

Java WeakHashMap详解(附源码分析)

相关推荐
legend_jz3 分钟前
【Linux】线程控制
linux·服务器·开发语言·c++·笔记·学习·学习方法
Komorebi.py4 分钟前
【Linux】-学习笔记04
linux·笔记·学习
drebander15 分钟前
使用 Java Stream 优雅实现List 转化为Map<key,Map<key,value>>
java·python·list
乌啼霜满天24918 分钟前
Spring 与 Spring MVC 与 Spring Boot三者之间的区别与联系
java·spring boot·spring·mvc
tangliang_cn23 分钟前
java入门 自定义springboot starter
java·开发语言·spring boot
程序猿阿伟24 分钟前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
Grey_fantasy34 分钟前
高级编程之结构化代码
java·spring boot·spring cloud
新知图书35 分钟前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
威威猫的栗子37 分钟前
Python Turtle召唤童年:喜羊羊与灰太狼之懒羊羊绘画
开发语言·python
力透键背37 分钟前
display: none和visibility: hidden的区别
开发语言·前端·javascript