WeakHashMap:Java中的内存优化利器

174 阅读6分钟

引言

在实际开发中,我们经常用到缓存,常见的就是使用Redismemcached等;但是实际还有许许多多场景,我们也想做缓存,但是数据又是可以随着资源紧张来丢弃的;这个时候使用Redismemcached就有点繁琐了,那么今天我们来认识一个做内存缓存的利器:WeakHashMap

一、认识WeakHashMap

image.png

从上面的图,我们可以看出,WeakHashMap也是我们集合的大家族里面的一员,所以他是满足Map这里面的基本特性的,那我们具体看看他自己有什么特点,如何工作的呢?

1.1 先回顾一下HashMap的工作原理和特性

特性类别描述
工作原理
哈希表HashMap基于哈希表实现,存储键值对。
哈希函数通过哈希函数计算键的哈希值,确定键值对在哈希表中的存储位置。
处理冲突使用链表或红黑树(Java 8及以后版本)来解决哈希冲突。
动态扩容元素数量达到一定阈值时,自动进行扩容,重新计算所有元素位置。
特性
允许空键和空值HashMap允许键或值为null。
非同步不是线程安全的。可以通过Collections.synchronizedMap或ConcurrentHashMap实现线程安全。
迭代顺序迭代顺序是插入顺序,直到结构上发生变化(如插入或删除操作)。
性能提供接近常数时间的性能(平均情况下),最坏情况下退化到线性时间。
初始容量和加载因子影响HashMap的性能和空间消耗。初始容量是哈希表创建时的容量,加载因子是容量自动增加前的满度阈值。

1.2 瞧瞧WeakHashMap的特点

  • 键是弱引用:键可能在不被使用时自动从映射中消失。

  • 不保证映射有序:迭代WeakHashMap时元素的顺序是不确定的,因为垃圾回收可能在任何时候移除键。

  • 快速失败迭代器WeakHashMap的迭代器是一个快速失败迭代器,如果映射结构在迭代过程中被修改,迭代器会快速失败。

1.3 WeakHashMap的工作原理

特性描述
存储机制WeakHashMap是基于哈希表的Map实现,内部使用一个Entry数组存储键值对。每个Entry是一个单向链表节点,包含键值对和指向下一个节点的引用。
键(Key)的弱引用键是弱引用的,如果没有被其他地方强引用,垃圾收集器可以回收该键及其关联的值。一旦键被回收,其在Map中的条目也会被自动移除。
值(Value)的存储值是强引用的,即使键被回收,如果没有其他地方引用这些值,它们也不会被垃圾收集器回收。值一直被保留,直到被显式移除或其键被回收。

二、 何时使用WeakHashMap

前面我们已经认识了WeakHashMap,那我们现在来看看如何使用它呢?直接上案例:

import org.apache.poi.ss.formula.functions.T;

import javax.imageio.ImageIO;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import java.util.WeakHashMap;

public class WeekHashMapCache {

    private static Map<String, Object> cache = Collections.synchronizedMap(new WeakHashMap<>());

    /**
     * 添加缓存
     *
     * @param key 键
     * @param obj 值
     */
    public static void add(String key, Object obj) {
        cache.put(key, obj);
    }

    /**
     * 查询缓存
     *
     * @param key   键
     * @param clazz 泛型对象
     * @return 具体值
     */
    public static Optional<T> get(String key, Class<T> clazz) {
        Object o = cache.get(key);
        if (Objects.isNull(o)) {
            return Optional.empty();
        }
        try {
            return Optional.ofNullable(clazz.cast(o));
        } catch (ClassCastException e) {
            // 如果类型转换失败,返回 Optional.empty()
            return Optional.empty();
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        String imageKey = "test001";
        Image image = ImageIO.read(new File("C:\Users\Admin\Pictures\wztt\zl\Snipaste_2024-08-16_17-59-55.png"));

        // 1. 缓存图片(imageKey是强引用)
        add(imageKey, image);
        // 输出
        System.out.println("数据存在: " + cache.get(imageKey));

    }
}

特别要注意的是,若 Value 对象反向持有 Key 的强引用(如 map.put(key, key.toString())),会导致内存泄漏,需避免此设计。

三、 WeakHashMap的局限性

实际看了上面的工作原理和简单案例,大家应该也发现了WeakHashMap有一些天然的局限,下面我们来仔细看看。

3.1 键回收依赖垃圾回收机制,不可控

  • WeakHashMap 的键通过弱引用关联,‌回收时机完全由 JVM 的垃圾回收器决定‌,开发者无法主动控制。即使调用 System.gc() 也仅是建议,无法保证立即回收。

  • 表现示例‌:若键不再被强引用但尚未触发 GC,条目仍会占用内存,导致内存释放延迟。

3.2 ‌Value 未弱引用,可能导致内存泄漏

  • WeakHashMap仅对键使用弱引用‌,Value 仍保持强引用。若 Value 直接或间接强引用 Key(如 Value = key.toString()),即使 Key 被回收,Value 仍无法释放。

  • 典型场景‌:缓存系统中,若 Value 包含对 Key 的反向引用,会导致内存泄漏。

3.3 ‌线程不安全,需额外同步

  • WeakHashMap非线程安全‌,多线程并发修改可能导致数据不一致。需通过 Collections.synchronizedMap 包装或手动加锁。

  • 问题示例‌:并发调用 put() 和 get() 时,可能触发 ConcurrentModificationException

3.4 ‌不适合长期缓存场景

  • 键可能随时被回收的特性导致 WeakHashMap不适合需要长期保留数据的缓存‌。若缓存项被意外回收,需频繁重新加载数据,影响性能。

  • 对比方案‌:长期缓存建议使用 ConcurrentHashMap 或 Guava Cache

3.5 ‌性能开销与不确定性

  • 内部维护 ‌引用队列(ReferenceQueue) ‌ 和自动清理机制会引入额外性能开销,高频操作时影响效率。

  • 不确定性表现‌:调用 size() 或 isEmpty() 时结果可能不准确(清理操作非实时触发)。

3.6 ‌键为 null 时的特殊行为

  • WeakHashMap 允许键为 null,但若显式使用 null 作为键,可能导致条目无法被正常清理或引发逻辑错误。

  • 示例风险‌:map.put(null, value) 后,若其他逻辑依赖键非空,可能触发空指针异常。

四、最佳实践

在第二点里面我给了一个使用WeakHashMap的案例,但是第三点我又写了很多使用的额局限性,这些局限性实际暴露了很多问题,如果一不小心,就会爆出一个大雷,那么怎么样才能更好的使用呢?

首先我们要理解,WeakHashMap的核心就是使用弱引用来做内存缓存,下面我将给出一个变体的解决方案。

Cache<String, String> cache = CacheBuilder.newBuilder()
        .weakValues()  // 值使用弱引用
        .maximumSize(1000)  // 额外容量限制
        .expireAfterWrite(10, TimeUnit.MINUTES)  // 写入10分钟后过期
        .build();

五、总结

可能有人发现第四点解决方案最终不是使用的WeakHashMap,确实是,这篇文章实际写到这里,相信大家也理解了,当自己把握不住的时候,找一个权威的三方组件,就是最好的办法,当然能够自己研究弄透彻更好。

希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。