MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

探究Java WeakHashMap的弱引用机制

2023-12-077.4k 阅读

Java 中的引用类型概述

在深入探讨 WeakHashMap 的弱引用机制之前,我们先来回顾一下 Java 中的引用类型。Java 提供了四种不同强度的引用类型,分别是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)。

强引用

强引用是我们最常用的引用类型。当一个对象被强引用所指向时,只要强引用存在,垃圾回收器就永远不会回收这个对象。例如:

Object strongRef = new Object();

在上述代码中,strongRef 就是一个强引用,只要 strongRef 变量在作用域内,它所指向的 Object 对象就不会被垃圾回收。即使内存不足,JVM 宁愿抛出 OutOfMemoryError 错误,也不会回收被强引用指向的对象。

软引用

软引用所指向的对象在系统内存充足时不会被回收,但当系统内存不足时,垃圾回收器会回收这些对象。软引用通常用于实现对内存敏感的缓存。可以通过 SoftReference 类来创建软引用,示例代码如下:

SoftReference<String> softRef = new SoftReference<>(new String("Soft Referenced Object"));
String value = softRef.get();
if (value != null) {
    // 使用软引用指向的对象
} else {
    // 软引用指向的对象已被回收
}

软引用适用于缓存那些可以在需要时重新创建的对象,这样在内存紧张时可以释放这些对象以避免 OutOfMemoryError

弱引用

弱引用是我们重点关注的引用类型,它的强度比软引用更弱。被弱引用指向的对象,只要垃圾回收器扫描到它,无论当前内存是否充足,都会回收该对象。WeakHashMap 正是利用了弱引用的这一特性。创建弱引用的示例代码如下:

WeakReference<String> weakRef = new WeakReference<>(new String("Weak Referenced Object"));
String value = weakRef.get();
if (value != null) {
    // 使用弱引用指向的对象
} else {
    // 弱引用指向的对象已被回收
}

在垃圾回收器运行时,一旦发现只有弱引用指向的对象,就会立即回收该对象。这意味着弱引用指向的对象生命周期可能非常短暂。

虚引用

虚引用是最弱的一种引用类型,它主要用于跟踪对象被垃圾回收的活动。虚引用必须和 ReferenceQueue 联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。通过检查引用队列中是否存在虚引用,可以判断对象是否即将被回收。示例代码如下:

ReferenceQueue<String> queue = new ReferenceQueue<>();
PhantomReference<String> phantomRef = new PhantomReference<>(new String("Phantom Referenced Object"), queue);
// 这里不能通过phantomRef.get()获取对象,因为虚引用无法直接访问对象

虚引用通常用于实现对象销毁前的一些特殊操作,比如资源清理等。

WeakHashMap 的基本原理

WeakHashMap 是 Java 集合框架中的一个实现类,它与普通的 HashMap 类似,用于存储键值对。但是,WeakHashMap 的独特之处在于,它使用弱引用(WeakReference)来引用其键。这意味着,当一个键在系统的其他地方不再有强引用指向它时,即使 WeakHashMap 中还持有该键的引用,这个键(以及对应的键值对)也可能会被垃圾回收器回收。

数据结构

WeakHashMap 内部使用数组(称为 table)和链表(在 JDK 1.8 之前)或红黑树(在 JDK 1.8 及之后,当链表长度达到一定阈值时会转换为红黑树)来实现哈希表结构。每个数组元素(Entry)存储了键值对以及指向下一个 Entry 的引用(形成链表结构)。不同之处在于,WeakHashMapEntry 类继承自 WeakReference,从而实现对键的弱引用。以下是简化的 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;

    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;
    }
    // 其他实现Map.Entry接口的方法
}

从上述代码可以看出,Entry 类继承自 WeakReference,在构造函数中通过 super(key, queue) 将键作为弱引用的对象,并关联了一个 ReferenceQueue。这个 ReferenceQueue 用于接收被垃圾回收的键的弱引用对象。

工作流程

  1. 插入操作:当向 WeakHashMap 中插入一个键值对时,会先计算键的哈希值,然后根据哈希值找到对应的数组位置。如果该位置为空,则直接插入新的 Entry;如果该位置已有 Entry,则通过链表(或红黑树)来处理哈希冲突。
WeakHashMap<String, Integer> weakMap = new WeakHashMap<>();
weakMap.put("key1", 1);
  1. 查找操作:查找时同样先计算键的哈希值,找到对应的数组位置,然后在链表(或红黑树)中查找与给定键相等的 Entry。如果找到,则返回对应的值;否则返回 null
Integer value = weakMap.get("key1");
  1. 垃圾回收与清理:当垃圾回收器运行时,如果发现某个键不再有强引用指向它,就会回收该键所占用的内存。同时,与该键关联的 WeakHashMap.Entry 对象会被添加到 ReferenceQueue 中。WeakHashMap 内部有一个方法 expungeStaleEntries,会在一些操作(如 getputremove 等)中被调用,用于从 ReferenceQueue 中移除已被回收的键对应的 Entry,从而清理 WeakHashMap 中的无效键值对。

深入剖析 WeakHashMap 的实现细节

构造函数

WeakHashMap 提供了多个构造函数,最常用的是无参构造函数和带初始容量和负载因子的构造函数。

  1. 无参构造函数
public WeakHashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

该构造函数调用了带初始容量和负载因子的构造函数,并使用默认的初始容量(16)和负载因子(0.75)。

  1. 带初始容量和负载因子的构造函数
public WeakHashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    this.loadFactor = loadFactor;
    table = new Entry<?,?>[tableSizeFor(initialCapacity)];
    threshold = (int)(table.length * loadFactor);
    queue = new ReferenceQueue<>();
}

在这个构造函数中,首先对传入的初始容量和负载因子进行合法性检查。然后根据初始容量计算出合适的数组大小(通过 tableSizeFor 方法,该方法会返回大于或等于初始容量的最小的 2 的幂次方),并初始化 table 数组。同时,设置负载因子和阈值,并创建一个 ReferenceQueue 用于接收被回收的键的弱引用对象。

put 方法

put 方法用于向 WeakHashMap 中插入键值对。其实现代码如下:

public V put(K key, V value) {
    Object k = maskNull(key);
    int h = hash(k);
    int i = indexFor(h, table.length);

    for (Entry<K, V> e = (Entry<K, V>) table[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 = (Entry<K, V>) table[i];
    table[i] = new Entry<>(k, value, queue, h, e);
    if (++size >= threshold)
        resize(table.length * 2);
    return null;
}
  1. 键的处理:首先调用 maskNull 方法处理 null 键,在 WeakHashMap 中,null 键会被特殊处理为一个内部定义的 NULL_KEY 对象。
  2. 哈希计算与索引查找:计算键的哈希值 h,并根据哈希值找到对应的数组索引 i
  3. 查找与更新:在链表(或红黑树)中查找与给定键相等的 Entry。如果找到,则更新其值并返回旧值。
  4. 插入新 Entry:如果未找到,则创建一个新的 Entry 并插入到链表头部(或红黑树中)。
  5. 容量调整:插入后检查当前元素个数 size 是否达到阈值 threshold,如果达到则进行扩容操作,将数组大小翻倍。

get 方法

get 方法用于从 WeakHashMap 中获取指定键对应的值。其实现代码如下:

public V get(Object key) {
    Object k = maskNull(key);
    int h = hash(k);
    Entry<K, V> e = (Entry<K, V>) getEntry(h, k);
    return (e == null? null : e.value);
}

private Entry<K, V> getEntry(int h, Object k) {
    Entry[] tab = table;
    int index = indexFor(h, tab.length);
    Entry<K, V> e = (Entry<K, V>) tab[index];
    while (e != null) {
        if (e.hash == h && eq(k, e.get()))
            return e;
        e = e.next;
    }
    return null;
}
  1. 键的处理:同样先调用 maskNull 方法处理 null 键。
  2. 哈希计算与索引查找:计算键的哈希值 h,并找到对应的数组索引 index
  3. 查找 Entry:从该索引位置开始,在链表(或红黑树)中查找与给定键相等的 Entry。如果找到,则返回该 Entry;否则返回 null

垃圾回收与清理

WeakHashMap 依赖垃圾回收器来回收不再被强引用指向的键,并通过 ReferenceQueue 来清理对应的 Entry。当垃圾回收器回收一个键时,与之关联的 WeakHashMap.Entry 对象会被添加到 ReferenceQueue 中。WeakHashMap 内部的 expungeStaleEntries 方法负责从 ReferenceQueue 中移除已被回收的键对应的 Entry。该方法在 getputremove 等操作中被调用。

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 = (Entry<K,V>) 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; // Help GC
                    size--;
                    break;
                }
                prev = p;
                p = next;
            }
        }
    }
}
  1. 从队列中取出引用:通过 queue.poll() 方法从 ReferenceQueue 中取出已被回收的键对应的 WeakHashMap.Entry 对象。
  2. 遍历链表(或红黑树):找到该 EntryWeakHashMap 中的位置,并从链表(或红黑树)中移除该 Entry
  3. 清理操作:将 Entry 的值设为 null,帮助垃圾回收,并减少 WeakHashMap 的元素个数 size

WeakHashMap 的应用场景

缓存应用

WeakHashMap 非常适合用于实现缓存,特别是对于那些缓存数据可以在需要时重新创建的场景。例如,在一个图片加载框架中,可以使用 WeakHashMap 来缓存已经加载过的图片。当内存紧张时,垃圾回收器会自动回收不再被强引用指向的图片缓存,从而避免内存溢出。示例代码如下:

import java.util.WeakHashMap;

public class ImageCache {
    private static WeakHashMap<String, byte[]> imageCache = new WeakHashMap<>();

    public static byte[] getImage(String imagePath) {
        byte[] image = imageCache.get(imagePath);
        if (image == null) {
            // 从文件或网络加载图片
            image = loadImageFromSource(imagePath);
            imageCache.put(imagePath, image);
        }
        return image;
    }

    private static byte[] loadImageFromSource(String imagePath) {
        // 实际的图片加载逻辑
        return new byte[0];
    }
}

在上述代码中,ImageCache 类使用 WeakHashMap 来缓存图片。当调用 getImage 方法时,如果图片已在缓存中,则直接返回;否则加载图片并添加到缓存中。由于使用了 WeakHashMap,当内存不足时,不再被强引用指向的图片缓存会被自动回收。

临时数据存储

在一些情况下,我们可能需要存储一些临时数据,这些数据在不再被使用时应该尽快释放内存。WeakHashMap 可以满足这种需求。例如,在一个 Web 应用中,可能需要在请求处理过程中临时存储一些用户相关的数据,当请求处理完成后,这些数据不再需要,可以通过 WeakHashMap 来存储这些临时数据,让垃圾回收器自动清理不再被使用的数据。示例代码如下:

import java.util.WeakHashMap;

public class RequestData {
    private static WeakHashMap<Thread, UserData> requestDataMap = new WeakHashMap<>();

    public static void setRequestData(UserData data) {
        requestDataMap.put(Thread.currentThread(), data);
    }

    public static UserData getRequestData() {
        return requestDataMap.get(Thread.currentThread());
    }
}

class UserData {
    // 用户相关数据字段
}

在上述代码中,RequestData 类使用 WeakHashMap 来存储每个线程的临时用户数据。当线程结束后,与之关联的 UserData 对象如果不再有其他强引用指向它,就会被垃圾回收器回收。

避免内存泄漏

在一些复杂的应用程序中,如果使用普通的 HashMap 来存储对象引用,可能会导致内存泄漏。例如,在一个观察者模式的应用中,如果使用普通 HashMap 来存储观察者对象,当被观察对象不再被使用,但观察者对象仍然被 HashMap 强引用时,这些观察者对象就无法被回收,从而导致内存泄漏。而使用 WeakHashMap 可以避免这种情况,因为当观察者对象不再被其他地方强引用时,垃圾回收器会自动回收它们。示例代码如下:

import java.util.WeakHashMap;

interface Observer {
    void update();
}

class Subject {
    private WeakHashMap<Observer, Void> observers = new WeakHashMap<>();

    public void addObserver(Observer observer) {
        observers.put(observer, null);
    }

    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers() {
        for (Observer observer : observers.keySet()) {
            if (observer != null) {
                observer.update();
            }
        }
    }
}

在上述代码中,Subject 类使用 WeakHashMap 来存储观察者对象。当某个观察者对象不再被其他地方强引用时,垃圾回收器会自动回收它,从而避免了内存泄漏。

WeakHashMap 的注意事项

线程安全性

WeakHashMap 不是线程安全的。如果多个线程同时访问和修改 WeakHashMap,可能会导致数据不一致或其他未定义行为。在多线程环境下使用 WeakHashMap 时,需要采取额外的同步措施,例如使用 Collections.synchronizedMap 方法将 WeakHashMap 包装成线程安全的 Map,或者使用 ConcurrentHashMap 等线程安全的集合类。示例代码如下:

import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;

WeakHashMap<String, Integer> weakMap = new WeakHashMap<>();
Map<String, Integer> synchronizedWeakMap = Collections.synchronizedMap(weakMap);

在上述代码中,通过 Collections.synchronizedMap 方法将 WeakHashMap 包装成线程安全的 Map,在多线程环境下可以安全地访问和修改。

键的生命周期

由于 WeakHashMap 使用弱引用指向键,键的生命周期可能会比预期的更短。在使用 WeakHashMap 时,需要确保对键的使用不会因为键被提前回收而导致程序出现错误。例如,在从 WeakHashMap 中获取值时,需要先检查键是否已被回收,否则可能会得到 null 值而导致程序逻辑错误。示例代码如下:

WeakHashMap<String, Integer> weakMap = new WeakHashMap<>();
String key = new String("key1");
weakMap.put(key, 1);

// 假设这里key不再被其他地方强引用
key = null;

// 此时垃圾回收器可能已经回收了key
Integer value = weakMap.get("key1");
if (value != null) {
    // 使用value
} else {
    // 键可能已被回收,需要进行相应处理
}

在上述代码中,当 key 不再被其他地方强引用时,垃圾回收器可能会回收 key 以及与之关联的 WeakHashMap.Entry。因此在获取值时,需要检查返回值是否为 null,以处理键被回收的情况。

性能问题

WeakHashMap 的性能与普通 HashMap 相比,在某些情况下可能会稍差。因为 WeakHashMap 内部需要处理垃圾回收和 ReferenceQueue 的操作,这些额外的操作会增加一定的开销。特别是在频繁插入、删除和查找操作的场景下,性能差异可能会更加明显。在选择使用 WeakHashMap 时,需要权衡内存管理和性能需求。如果对性能要求非常高,且内存使用不是主要问题,可能需要考虑其他集合类。

与其他集合类的比较

与 HashMap 的比较

  1. 引用类型HashMap 使用强引用指向键和值,只要 HashMap 本身存在且键值对未被移除,键和值对象就不会被垃圾回收。而 WeakHashMap 使用弱引用指向键,当键不再被其他地方强引用时,键(以及对应的键值对)可能会被垃圾回收。
  2. 内存管理WeakHashMap 更适合用于需要自动释放不再使用的键值对以节省内存的场景,例如缓存应用。而 HashMap 适用于需要确保数据始终存在,直到显式移除的场景。
  3. 性能:由于 WeakHashMap 额外的垃圾回收和 ReferenceQueue 处理,其性能在某些情况下可能不如 HashMap。特别是在频繁操作的场景下,HashMap 通常会有更好的性能表现。

与 SoftHashMap 的比较

  1. 引用强度WeakHashMap 使用弱引用,只要垃圾回收器扫描到键不再被强引用,就会回收键及对应的键值对。而 SoftHashMap(Java 中没有标准的 SoftHashMap 类,但可以通过自定义实现)使用软引用,只有在内存不足时才会回收键值对。
  2. 应用场景WeakHashMap 适用于对内存敏感,且数据可以在需要时重新创建的场景,例如临时数据存储。SoftHashMap 更适合用于缓存那些希望在内存充足时尽量保留,内存不足时释放的重要数据,例如图片缓存。
  3. 实现复杂度WeakHashMap 的实现相对简单,主要依赖 WeakReferenceReferenceQueue。而自定义实现 SoftHashMap 可能需要更复杂的逻辑来处理软引用和内存监控等。

与 ConcurrentHashMap 的比较

  1. 线程安全性ConcurrentHashMap 是线程安全的,允许多个线程同时访问和修改,并且提供了较高的并发性能。而 WeakHashMap 不是线程安全的,在多线程环境下需要额外的同步措施。
  2. 引用类型ConcurrentHashMap 使用强引用指向键和值,与 WeakHashMap 的弱引用机制不同。
  3. 应用场景ConcurrentHashMap 适用于多线程环境下需要高效并发访问的场景,例如在服务器端应用中存储共享数据。而 WeakHashMap 主要用于内存管理和单线程或需要手动同步的场景下的特殊需求,如缓存和临时数据存储。

通过对 WeakHashMap 的深入探究,我们了解了其弱引用机制、实现细节、应用场景以及与其他集合类的比较。在实际开发中,根据具体的需求和场景,合理选择使用 WeakHashMap 可以有效地优化内存使用,避免内存泄漏,并提高程序的性能和稳定性。同时,需要注意 WeakHashMap 的线程安全性和键的生命周期等问题,以确保程序的正确性和可靠性。