Java 的 WeakHashMap 通常被认为对于缓存很有用。尽管它的弱引用是根据映射的键而不是其值来定义的,但这似乎很奇怪。我的意思是,这是我想要缓存的值,并且一旦除了缓存之外没有其他人强烈引用它们,我就希望对其进行垃圾收集,不是吗?
以何种方式保存对键的弱引用有帮助?如果您执行
ExpensiveObject o = weakHashMap.get("some_key")
,那么我希望缓存保留“o”,直到调用者不再保留强引用,并且我根本不关心字符串对象“some_key”。
我错过了什么吗?
WeakHashMap 没有用作缓存,至少大多数人是这样认为的。正如你所说,它使用弱键,而不是弱值,所以它不是为大多数人想要使用它的目的而设计的(事实上,我已经见过人们错误地使用它)。
WeakHashMap 最适合保存有关您无法控制生命周期的对象的元数据。例如,如果您有一堆对象通过您的类,并且您希望跟踪有关它们的额外数据,而无需在它们超出范围时收到通知,并且无需引用它们来保持它们的活动状态。
一个简单的例子(我以前用过)可能是这样的:
WeakHashMap<Thread, SomeMetaData>
您可以在其中跟踪系统中各个线程正在做什么;当线程终止时,该条目将从您的映射中默默删除,并且如果您是对该线程的最后一个引用,则不会阻止该线程被垃圾收集。然后,您可以迭代该映射中的条目,以找出有关系统中活动线程的元数据。
请参阅非缓存中的WeakHashMap!了解更多信息。
对于您想要的缓存类型,可以使用专用的缓存系统(例如EHCache)或查看Guava的MapMaker类;类似的东西
new MapMaker().weakValues().makeMap();
会做你想做的事,或者如果你想变得更奇特,你可以添加定时过期:
new MapMaker().weakValues().expiration(5, TimeUnit.MINUTES).makeMap();
WeakHashMap
的主要用途是当您有想要在其键消失时消失的映射时。缓存则相反——当映射的值消失时,您希望这些映射也消失。
对于缓存,你想要的是一个
Map<K,SoftReference<V>>
。当内存紧张时,SoftReference
将被垃圾收集。 (将此与 WeakReference
进行对比,一旦不再对其所指对象进行硬引用,它可能会被清除。)您希望您的引用在缓存中是软引用(至少在键值映射不存在的缓存中) t go stale),从那时起,如果您稍后查找您的值,则有可能仍然在缓存中。如果引用很弱,您的值将立即被GC,从而违背了缓存的目的。
为了方便起见,您可能希望隐藏
SoftReference
实现中的 Map
值,以便您的缓存看起来是 <K,V>
类型而不是 <K,SoftReference<V>>
类型。如果您想这样做,这个问题有网上可用的实现建议。
另请注意,当您在
SoftReference
中使用 Map
值时,您必须执行某些操作来手动删除已清除 SoftReferences
的键值对——否则您的 Map
只会增长永远大小,并泄漏内存。
您需要两张映射:一张在缓存键和弱引用值之间映射,一张在弱引用值和键之间反向映射。并且您需要一个引用队列和一个清理线程。
弱引用能够在无法再访问引用的对象时将引用移动到队列中。该队列必须由清理线程清空。 为了清理,需要获取密钥作为参考。这就是需要第二张地图的原因。
以下示例展示了如何使用弱引用的哈希图创建缓存。当您运行该程序时,您会得到以下输出:
$ javac -Xlint:未选中的 Cache.java && java 缓存 {偶数:[2,4,6],奇数:[1,3,5]} {偶数: [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();
}
}
另一件需要考虑的事情是,如果采用
Map<K, WeakReference<V>>
方法,值可能会消失,但映射不会。根据使用情况,您最终可能会得到一个包含许多弱引用已被 GC 处理的条目的 Map。
如果您需要弱值,这非常简单:
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