集合-WeakHashMap源码
集合-WeakHashMap源码
核理論
1.1 类结构分析
WeakHashMap是Java集合框架中一种特殊的Map实现,它使用弱引用(WeakReference)作为键(key),当键对象不再被强引用时,对应的键值对会被自动移除。这种特性使得WeakHashMap非常适合实现缓存功能,能够自动释放不再使用的对象,避免内存泄漏。
1.2 弱引用机制
在Java中,引用分为四种级别:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。WeakHashMap使用弱引用作为键,这意味着当键对象没有被其他强引用指向时,可能会被垃圾回收器回收,即使此时它仍然是WeakHashMap的键。
1.3 JDK版本特性
- JDK 1.2:WeakHashMap首次被引入
- JDK 8:实现了Map.Entry接口的spliterator()方法
- JDK 9+:添加了of()、copyOf()等静态工厂方法,支持创建不可变WeakHashMap实例
代码实践
2.1 核心成员变量
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
    // 存储键值对的数组
    private Entry<K,V>[] table;
    // 键值对数量
    private int size;
    // 修改计数器,用于快速失败机制
    private int modCount;
    // 扩容阈值
    private int threshold;
    // 负载因子
    private final float loadFactor;
    // 引用队列,用于存储被回收的键
    private final ReferenceQueue<K> queue = new ReferenceQueue<>();
    // ...
}2.2 Entry节点实现
private static class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V> {
    V value;
    final int hash;
    Entry<K,V> next;
    /**
     * 创建一个新的Entry节点
     * @param key 键,将被包装为弱引用
     * @param value 值,强引用
     * @param queue 引用队列,当键被回收时会加入此队列
     * @param hash 键的哈希值
     * @param next 下一个节点
     */
    Entry(K key, V value, ReferenceQueue<K> queue, int hash, Entry<K,V> next) {
        super(key, queue);
        this.value = value;
        this.hash = hash;
        this.next = next;
    }
    // Entry接口实现
    public K getKey() { return get(); }
    public V getValue() { return value; }
    public V setValue(V newValue) { ... }
    public boolean equals(Object o) { ... }
    public int hashCode() { ... }
}2.3 清理过期条目
expungeStaleEntries()方法是WeakHashMap的核心,它负责从表中移除那些键已经被回收的条目:
/**
 * 清理所有键已被回收的条目
 */
private void expungeStaleEntries() {
    Entry<K,V> e;
    // 循环从引用队列中获取被回收的键对应的Entry
    while ((e = (Entry<K,V>) queue.poll()) != null) {
        int h = e.hash;
        int i = indexFor(h, 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;
                e.value = null; // 帮助GC回收value
                size--;
            } else {
                int i = indexFor(e.hash, dest.length);
                e.next = dest[i];
                dest[i] = e;
            }
            e = next;
        }
    }
}2.4 get方法实现
/**
 * 根据键获取值
 * @param key 要查找的键
 * @return 键对应的值,如果键不存在或已被回收则返回null
 */
public V get(Object key) {
    Object k = maskNull(key);
    int h = hash(k);
    Entry<K,V>[] tab = getTable();
    int index = indexFor(h, tab.length);
    Entry<K,V> e = tab[index];
    // 遍历链表查找
    while (e != null) {
        if (e.hash == h && eq(k, e.get()))
            return e.value;
        e = e.next;
    }
    return null;
}
/**
 * 获取哈希表前先清理过期条目
 */
private Entry<K,V>[] getTable() {
    expungeStaleEntries();
    return table;
}2.5 put方法实现
/**
 * 添加键值对到映射中
 * @param key 键
 * @param value 值
 * @return 之前与key关联的值,如果没有则返回null
 */
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];
    // 创建新的Entry并添加到链表头部
    tab[i] = new Entry<>(k, value, queue, h, e);
    // 检查是否需要扩容
    if (++size >= threshold)
        resize(tab.length * 2);
    return null;
}2.6 扩容机制
/**
 * 调整哈希表大小
 * @param newCapacity 新容量
 * @return 调整后的哈希表
 */
private Entry<K,V>[] resize(int newCapacity) {
    Entry<K,V>[] oldTable = getTable();
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return oldTable;
    }
    Entry<K,V>[] newTable = new Entry[newCapacity];
    // 转移条目到新表
    transfer(oldTable, newTable);
    table = newTable;
    // 计算新的扩容阈值
    threshold = (int)(newCapacity * loadFactor);
    return newTable;
}
/**
 * 将条目从旧表转移到新表
 */
private void transfer(Entry<K,V>[] src, Entry<K,V>[] dest) {
    for (int j = 0; j < src.length; ++j) {
        Entry<K,V> e = src[j];
        src[j] = null;
        while (e != null) {
            Entry<K,V> next = e.next;
            K key = e.get();
            if (key == null) {
                e.next = null;  // 帮助GC
                e.value = null; // 帮助GC回收value
                size--;
            } else {
                int i = indexFor(e.hash, dest.length);
                e.next = dest[i];
                dest[i] = e;
            }
            e = next;
        }
    }
}2.7 WeakHashMap缓存示例
import java.util.WeakHashMap;
/**
 * 使用WeakHashMap实现简单缓存
 */
public class WeakHashMapCacheExample {
    private final WeakHashMap<String, Object> cache = new WeakHashMap<>();
    /**
     * 从缓存获取对象
     */
    public Object get(String key) {
        return cache.get(key);
    }
    /**
     * 添加对象到缓存
     */
    public void put(String key, Object value) {
        cache.put(key, value);
    }
    /**
     * 清除缓存
     */
    public void clear() {
        cache.clear();
    }
    public static void main(String[] args) {
        WeakHashMapCacheExample cache = new WeakHashMapCacheExample();
        String key = new String("testKey");
        Object value = new Object();
        cache.put(key, value);
        System.out.println("缓存中获取值: " + (cache.get(key) != null)); // true
        // 移除强引用
        key = null;
        value = null;
        // 触发GC
        System.gc();
        // 短暂等待GC完成
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 缓存中的条目已被自动移除
        System.out.println("GC后缓存中获取值: " + (cache.get("testKey") != null)); // false
    }
}设计思想
3.1 数据结构设计
WeakHashMap的核心设计思想是结合哈希表和弱引用机制,实现自动过期的键值对存储。它的内部结构与HashMap类似,都使用数组+链表的结构来存储键值对,但有以下关键区别:
- 键的存储方式:WeakHashMap的键被包装在WeakReference中,而HashMap使用强引用
- 引用队列:WeakHashMap维护一个引用队列,用于跟踪被回收的键
- 自动清理机制:在每次操作前会清理过期条目
3.2 自动清理策略
WeakHashMap采用懒清理策略,在以下几种情况下会触发过期条目的清理:
- 调用get()、put()、remove()等方法时
- 调用size()、isEmpty()等查询方法时
- 遍历集合视图时(如keySet()、entrySet())
这种策略避免了专门的清理线程,简化了实现并提高了并发性能,但可能导致已回收键对应的条目不会立即从表中移除。
3.3 性能权衡
与HashMap相比,WeakHashMap在性能上有以下特点:
- 插入性能:略低于HashMap,因为需要创建WeakReference包装键
- 查询性能:类似HashMap,但需要额外处理过期条目
- 内存占用:通常更低,因为不再使用的键值对会被自动回收
- GC影响:GC可能会导致条目被移除,影响迭代行为
避坑指南
4.1 键的强引用问题
问题:如果键对象仍然被强引用,即使不再需要,WeakHashMap也不会自动移除对应的条目。
解决方案:确保只将临时对象或明确希望被自动回收的对象作为WeakHashMap的键,避免意外的强引用。
// 错误示例:字符串常量池中的字符串是强引用,不会被回收
WeakHashMap<String, Object> map = new WeakHashMap<>();
map.put("permanentKey", new Object()); // 键是常量池中的字符串,永远不会被回收
// 正确示例:使用new创建的字符串对象,可以被回收
WeakHashMap<String, Object> map = new WeakHashMap<>();
map.put(new String("temporaryKey"), new Object()); // 键是新创建的对象,没有其他强引用时可以被回收4.2 线程安全问题
问题:WeakHashMap不是线程安全的,多线程环境下可能导致并发修改异常或数据不一致。
解决方案:使用Collections.synchronizedMap()包装,或使用ConcurrentHashMap(如果不需要弱引用特性)。
// 线程安全的WeakHashMap
Map<K, V> safeMap = Collections.synchronizedMap(new WeakHashMap<>());4.3 值的内存泄漏
问题:虽然键是弱引用,但值是强引用。如果值对象很大且生命周期长,即使键被回收,值也可能导致内存泄漏。
解决方案:考虑使用弱引用包装值,或使用专门的缓存框架如Guava Cache。
// 使用弱引用包装值
WeakHashMap<K, WeakReference<V>> map = new WeakHashMap<>();
map.put(key, new WeakReference<>(value));4.4 迭代行为不可预测
问题:在迭代过程中,GC可能回收键导致条目被移除,产生不可预测的迭代行为。
解决方案:迭代前获取快照,或使用同步块确保迭代过程中不发生GC。
// 获取键集快照后迭代
Set<K> keySet;
synchronized (map) {
    keySet = new HashSet<>(map.keySet());
}
for (K key : keySet) {
    // 使用key访问map
}深度思考题
思考题1:WeakHashMap与HashMap的红黑树实现有何不同?
思考题回答:WeakHashMap在JDK 8及之后的版本中没有像HashMap那样引入红黑树优化。这是因为WeakHashMap的设计目标和使用场景与HashMap不同:
- WeakHashMap通常用于存储临时对象,条目数量一般不会太大
- 键的生命周期不确定,红黑树的维护成本可能超过其带来的性能收益
- 频繁的GC回收可能导致树结构频繁重组,降低性能
如果需要高性能的大容量弱引用映射,可能需要考虑第三方库或自定义实现。
思考题2:如何实现一个线程安全的WeakHashMap?
思考题回答:实现线程安全的WeakHashMap可以有以下几种方案:
- 使用Collections.synchronizedMap():最简单的方式,但并发性能较差
- 分段锁实现:类似ConcurrentHashMap的分段锁机制,将表分成多个段,每个段独立加锁
- 读写锁:使用ReentrantReadWriteLock分离读写操作,提高并发读性能
- ConcurrentReferenceHashMap:Guava库提供的并发引用映射实现,比WeakHashMap更适合高并发场景
以下是一个基于读写锁的简单实现:
public class ConcurrentWeakHashMap<K, V> {
    private final WeakHashMap<K, V> map = new WeakHashMap<>();
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    public V get(K key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }
    public V put(K key, V value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
    // 其他方法...
}思考题3:WeakHashMap、SoftReference和PhantomReference在缓存实现中有何区别?
思考题回答:在缓存实现中,这三种引用类型有明显区别:
- WeakHashMap(弱引用):键被GC时立即回收,适合存储临时对象缓存
- SoftReference(软引用):内存不足时才回收,适合实现内存敏感的缓存
- PhantomReference(虚引用):仅用于跟踪对象回收,无法通过虚引用获取对象,不适合缓存
选择建议:
- 频繁使用且内存占用小的对象:WeakHashMap
- 不常使用但内存占用大的对象:SoftReference
- 需要在对象回收前执行清理操作:PhantomReference
Java 9引入的Cleaner API结合PhantomReference提供了更灵活的对象清理机制,可以替代finalize()方法。
