【Java】WeakHashMap详解:原理、实现与实战

大家好,我是云扬~ 今天想和大家深入聊聊 Java 集合框架中一个特殊的存在 ——WeakHashMap。它和我们常用的 HashMap 长得很像,但骨子里藏着 “自动回收” 的黑科技,在缓存设计、临时数据存储等场景中特别实用。这篇文章会从原理到代码,带大家彻底搞懂 WeakHashMap!

一、先搞懂:WeakHashMap 和 HashMap 的核心区别

我们平时用的 HashMap,key 是强引用—— 只要 HashMap 本身不被回收,key 和对应的 value 就会一直占着内存。而 WeakHashMap 的关键特性是:key 是弱引用,不会阻止垃圾回收(GC)

简单说:当 WeakHashMap 的 key 不再被其他强引用指向时,下一次 GC 就会回收这个 key,而 WeakHashMap 会自动删除对应的 key-value 对,避免内存泄漏。

二、Java 四种引用类型(基础必备)

要理解 WeakHashMap,必须先清楚 Java 的四种引用类型,这是它的核心底层支撑:

引用类型特点适用场景
强引用(默认)只要可达,绝不回收普通对象存储(如 HashMap 的 key)
软引用(SoftRef)内存不足时才回收缓存(如图片缓存)
弱引用(WeakRef)下一次 GC 必定回收WeakHashMap 的 key
虚引用(PhantomRef)无法获取对象,仅用于监听回收事件资源释放监听

WeakHashMap 正是利用了弱引用的特性,让 key 可以被自动回收。

三、WeakHashMap 的底层实现原理

1. Entry 继承 WeakReference

WeakHashMap 的内部类 Entry 并没有直接持有 key 的强引用,而是继承了 WeakReference,将 key 作为弱引用的目标对象:

// WeakHashMap 内部 Entry 源码简化
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;

    // key 被包装成弱引用,传入引用队列
    Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
        super(key, queue); // 调用 WeakReference 构造器,key 成为弱引用
        this.value = value;
        this.hash = hash;
        this.next = next;
    }

    // ... 其他方法
}

这里的关键是 super(key, queue):将 key 交给 WeakReference 管理,同时关联一个引用队列(ReferenceQueue)

2. 引用队列的作用

当 key 被 GC 标记为垃圾后,会被加入到这个引用队列中。WeakHashMap 在执行 put()get()size() 等方法时,会先检查引用队列,把已经失效的 Entry(key 已被回收)从哈希表中删除 —— 这就是 “自动清理” 的核心。

四、代码实战:验证 WeakHashMap 的特性

光说不练假把式,我们用代码验证 WeakHashMap 的 key 回收机制:

示例 1:key 无强引用时自动回收

import java.util.WeakHashMap;

public class WeakHashMapTest {
    public static void main(String[] args) {
        // 1. 创建 WeakHashMap
        WeakHashMap<String, String> weakMap = new WeakHashMap<>();

        // 2. key 是临时对象,无其他强引用
        String key = new String("云扬的key");
        weakMap.put(key, "WeakHashMap 测试值");

        // 3. 打印此时的 map 内容(有值)
        System.out.println("GC前:" + weakMap); // 输出:{云扬的key=WeakHashMap 测试值}

        // 4. 移除 key 的强引用(关键!)
        key = null;

        // 5. 手动触发 GC
        System.gc();
        // 暂停一下,让 GC 完成
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 6. 再次打印 map(key 已被回收,Entry 被自动删除)
        System.out.println("GC后:" + weakMap); // 输出:{}
    }
}

运行结果

GC前:{云扬的key=WeakHashMap 测试值}
GC后:{}

结论:当 key 没有强引用时,GC 会回收 key,WeakHashMap 自动删除对应的 Entry。

示例 2:key 有强引用时不回收

如果 key 还有其他强引用,即使放入 WeakHashMap,也不会被回收:

public class WeakHashMapStrongRefTest {
    public static void main(String[] args) {
        WeakHashMap<String, String> weakMap = new WeakHashMap<>();
        // 强引用持有 key
        String strongKey = new String("强引用key");
        weakMap.put(strongKey, "不会被回收的值");

        // 触发 GC
        System.gc();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // key 有强引用,不会被回收
        System.out.println("有强引用时:" + weakMap); // 输出:{强引用key=不会被回收的值}

        // 移除强引用
        strongKey = null;
        System.gc();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 此时 key 被回收
        System.out.println("移除强引用后:" + weakMap); // 输出:{}
    }
}

五、WeakHashMap 如何清理失效 Entry?

很多人会疑惑:key 被 GC 回收后,WeakHashMap 是怎么知道要删除对应的 Entry 的?

答案是:主动轮询 + 引用队列。WeakHashMap 并没有依赖 Java 的 “对象回收通知”(因为 Java 没有可靠的回调机制),而是在每次调用 put()get()size()isEmpty() 等方法时,都会先执行 expungeStaleEntries() 方法,遍历引用队列,删除所有失效的 Entry:

// WeakHashMap 核心清理方法(简化版)
private void expungeStaleEntries() {
    Entry e;
    // 遍历引用队列,取出所有已回收 key 对应的 Entry
    while ((e = (Entry.poll()) != null) {
        int hash = e.hash;
        int index = indexFor(hash, table.length);

        // 从哈希表中删除该 Entry(链表操作)
        Entry> prev = table[index];
        Entry p = prev;
        while (p != null) {
            Entry> next = p.next;
            if (p == e) {
                if (prev == e)
                    table[index] = next;
                else
                    prev.next = next;
                e.value = null; // 释放 value 的引用,帮助 GC
                size--;
                break;
            }
            prev = p;
            p = next;
        }
    }
}

这就是为什么我们在示例中触发 GC 后,WeakHashMap 会自动清空 —— 下次调用 toString()(间接调用 size())时,会触发清理逻辑。

六、使用场景与注意事项

1. 适合场景

  • 临时缓存:不需要手动清理,key 不再使用时自动回收(如缓存用户会话,会话过期后自动删除)。
  • 避免内存泄漏:当 key 是外部对象,且可能被回收时,用 WeakHashMap 不会阻止 GC(如 HashMap 若不手动删除 key,可能导致内存泄漏)。

2. 注意事项

  • value 可能内存泄漏:虽然 key 会被回收,但 value 仍可能被 Entry 持有强引用。如果 value 很大,建议在 key 回收后手动清理 value(可通过重写 finalize() 或使用引用队列监听)。
  • 线程不安全:和 HashMap 一样,WeakHashMap 不是线程安全的,多线程环境下需使用 Collections.synchronizedMap() 包装。
  • key 不能为 null:WeakHashMap 的 key 是弱引用,null 没有引用,会被直接回收,因此不允许 key 为 null(HashMap 允许 key 为 null)。

七、总结

WeakHashMap 的核心是弱引用 + 引用队列

  1. key 被包装成弱引用,不阻止 GC。
  2. key 被回收后,会加入引用队列。
  3. WeakHashMap 操作时(put/get/size),主动清理队列中的失效 Entry。

它是 Java 中解决 “自动回收临时数据” 的利器,尤其适合缓存场景。希望这篇文章能帮大家彻底搞懂 WeakHashMap,下次遇到类似需求时能灵活运用~

如果有疑问或补充,欢迎在评论区交流!

Tags:

发表回复

Your email address will not be published. Required fields are marked *.

*
*