Java 的 WeakHashMap 和缓存: 为什么它引用键而不是值?

Java 的 弱 HashMap经常被认为对缓存很有用。虽然它的弱引用是根据映射的键而不是它的值来定义的,这看起来很奇怪。我的意思是,它是我想要缓存的值,而且我希望在除了缓存之外没有其他人强烈引用它们的时候对它们进行垃圾收集,不是吗?

它通过哪种方式帮助保持对键的弱引用?如果执行 ExpensiveObject o = weakHashMap.get("some_key"),那么我希望缓存保持在‘ o’上,直到调用者不再保持强引用,而且我根本不关心字符串对象“ some _ key”。

我错过了什么吗?

28501 次浏览

WeakHashMap不是作为缓存非常有用,至少大多数人是这么认为的。正如你所说,它使用弱 钥匙,而不是弱 价值观,所以它不是为大多数人想要使用它的目的而设计的(事实上,我已经让 看见了的人使用它了,不正确)。

WeakHashMap 主要用于保存关于您不能控制其生命周期的对象的元数据。例如,如果有许多对象通过您的类,并且您希望跟踪关于它们的额外数据,而不需要在它们超出范围时通知它们,并且不需要对它们的引用来保持它们的活性。

一个简单的例子(我以前用过)可能是这样的:

WeakHashMap<Thread, SomeMetaData>

在这里,您可以跟踪系统中的各个线程正在做什么; 当线程死亡时,条目将从映射中悄悄地删除,而且如果您是线程的最后一个引用,则不会阻止垃圾收集线程。然后,您可以迭代该映射中的条目,以查找关于系统中活动线程的元数据。

有关更多信息,请参见 不在缓存中的弱 HashMap!

对于您想要的缓存类型,可以使用专用的缓存系统(例如 EHCache) ,或者查看 番石榴MapMaker 类; 类似于

new MapMaker().weakValues().makeMap();

会做你想做的事情,或者如果你想变得花哨,你可以加上时间过期:

new MapMaker().weakValues().expiration(5, TimeUnit.MINUTES).makeMap();

WeakHashMap的主要用途是当您拥有映射时,当它们的键消失时,您希望它们消失。缓存是相反的-,您有一些映射,当它们的值消失时,您希望它们消失。

对于缓存,需要的是 Map<K,SoftReference<V>>。当内存紧张时,将对 SoftReference进行垃圾回收。(与 WeakReference形成对比,WeakReference可能在不再有对其指向的硬引用时被清除。)您希望您的引用在缓存中是软的(至少在键-值映射不会过时的情况下) ,因为如果您稍后查找它们,那么您的值仍有可能在缓存中。如果引用比较弱,那么您的值将立即被 gc 化,从而破坏了缓存的目的。

为了方便起见,您可能希望在 Map实现中隐藏 SoftReference值,以便缓存显示为 <K,V>而不是 <K,SoftReference<V>>类型。如果你想这样做,这个问题有建议的实现可在网上。

还要注意的是,当您在 Map中使用 SoftReference值时,您的 必须的需要做一些事情来手动删除已经清除了 SoftReferences的键-值对——否则您的 Map只会永远增长大小,并且会泄漏内存。

另一件需要考虑的事情是,如果采用 Map<K, WeakReference<V>>方法,值可能会消失,但映射不会消失。根据用法的不同,您最终可能会得到一个包含许多弱引用已被 GC 分离的条目的 Map。

您需要两个映射: 一个映射在缓存键和 弱引用值之间,另一个映射在弱引用值和键之间。你需要一个 参考队列和一个清理线程。

弱引用能够在被引用对象不能再访问时将引用移动到队列中。此队列必须由清理线程排空。这就是为什么需要第二张地图的原因。

下面的示例演示如何使用弱引用的散列映射创建缓存。当你运行这个程序时,你会得到以下输出:

$ javac -Xlint:unchecked Cache.java && java Cache
{even: [2, 4, 6], odd: [1, 3, 5]}
{even: [2, 4, 6]}

第一行显示对奇数列表的引用被删除之前缓存的内容,第二行显示对奇数列表的引用被删除之后缓存的内容。

这是密码:

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


class Cache<K,V>
{
ReferenceQueue<V> queue = null;
Map<K,WeakReference<V>> values = null;
Map<WeakReference<V>,K> keys = null;
Thread cleanup = null;


Cache ()
{
queue  = new ReferenceQueue<V>();
keys   = Collections.synchronizedMap (new HashMap<WeakReference<V>,K>());
values = Collections.synchronizedMap (new HashMap<K,WeakReference<V>>());
cleanup = new Thread() {
public void run() {
try {
for (;;) {
@SuppressWarnings("unchecked")
WeakReference<V> ref = (WeakReference<V>)queue.remove();
K key = keys.get(ref);
keys.remove(ref);
values.remove(key);
}
}
catch (InterruptedException e) {}
}
};
cleanup.setDaemon (true);
cleanup.start();
}


void stop () {
cleanup.interrupt();
}


V get (K key) {
return values.get(key).get();
}


void put (K key, V value) {
WeakReference<V> ref = new WeakReference<V>(value, queue);
keys.put (ref, key);
values.put (key, ref);
}


public String toString() {
StringBuilder str = new StringBuilder();
str.append ("{");
boolean first = true;
for (Map.Entry<K,WeakReference<V>> entry : values.entrySet()) {
if (first)
first = false;
else
str.append (", ");
str.append (entry.getKey());
str.append (": ");
str.append (entry.getValue().get());
}
str.append ("}");
return str.toString();
}


static void gc (int loop, int delay) throws Exception
{
for (int n = loop; n > 0; n--) {
Thread.sleep(delay);
System.gc(); // <- obstinate donkey
}
}


public static void main (String[] args) throws Exception
{
// Create the cache
Cache<String,List> c = new Cache<String,List>();


// Create some values
List odd = Arrays.asList(new Object[]{1,3,5});
List even = Arrays.asList(new Object[]{2,4,6});


// Save them in the cache
c.put ("odd", odd);
c.put ("even", even);


// Display the cache contents
System.out.println (c);


// Erase one value;
odd = null;


// Force garbage collection
gc (10, 10);


// Display the cache again
System.out.println (c);


// Stop cleanup thread
c.stop();
}
}

如果你需要弱价值观,那么它非常简单:

public final class SimpleCache<K,V> {
private final HashMap<K,Ref<K,V>> map = new HashMap<>();
private final ReferenceQueue<V> queue = new ReferenceQueue<>();
    

private static final class Ref<K,V> extends WeakReference<V> {
final K key;
        

Ref(K key, V value, ReferenceQueue<V> queue) {
super(value, queue);
this.key = key;
}
}
    

private synchronized void gc() {
for (Ref<?,?> ref; (ref = (Ref<?,?>)queue.poll()) != null;)
map.remove(ref.key, ref);
}
    

public synchronized V getOrCreate(K key, Function<K,V> creator) {
gc();
Ref<K,V> ref = map.get(key);
V v = ref == null ? null : ref.get();
if (v == null) {
v = Objects.requireNonNull(creator.apply(key));
map.put(key, new Ref<>(key, v, queue));
}
return v;
}
    

public synchronized void remove(K key) {
gc();
map.remove(key);
}
}

不需要多个线程; 当调用其他方法时,可以通过轮询引用队列来删除陈旧的映射条目。(WeakHashMap 也是这样工作的。)

例如:

static final SimpleCache<File,BigObject> cache = new SimpleCache<>();


...


// if there is already a BigObject generated for this file,
// and it is hasn't been garbage-collected yet, it is returned;
// otherwise, its constructor is called to create one
BigObject bo = cache.getOrCreate(fileName, BigObject::new)
// it will be gc'd after nothing in the program keeps a strong ref any more