Java的WeakHashMap通常被认为是很有用的缓存工具。但奇怪的是,它的弱引用是按照映射的键而不是值来定义的。我的意思是,我想要缓存的是值,并且只有当除了缓存之外没有其他人强烈地引用它们时,我才想让它们被垃圾回收掉,对吧?
持有键的弱引用有什么帮助呢?如果你使用ExpensiveObject o = weakHashMap.get("some_key")
,那么我希望缓存会保存'o'直到调用者不再持有强引用,而我完全不关心字符串对象"some_key"。
我错过了什么吗?
Java的WeakHashMap通常被认为是很有用的缓存工具。但奇怪的是,它的弱引用是按照映射的键而不是值来定义的。我的意思是,我想要缓存的是值,并且只有当除了缓存之外没有其他人强烈地引用它们时,我才想让它们被垃圾回收掉,对吧?
持有键的弱引用有什么帮助呢?如果你使用ExpensiveObject o = weakHashMap.get("some_key")
,那么我希望缓存会保存'o'直到调用者不再持有强引用,而我完全不关心字符串对象"some_key"。
我错过了什么吗?
WeakHashMap通常不适用作缓存,至少大多数人所认为的那样。正如您所说,它使用弱键而不是弱值,因此它不是为大多数人想要使用它的方式设计的(事实上,我曾经看到有人错误地使用它)。
WeakHashMap主要用于保留关于您不控制生命周期的对象的元数据。例如,如果有一堆对象通过您的类,并且您想在不需要被通知它们作用域结束的情况下跟踪有关它们的额外数据,同时您对它们的引用不会使其继续存在。
一个简单的例子(我以前用过的例子)可能是:
WeakHashMap<Thread, SomeMetaData>
你可以使用WeakHashMap来跟踪系统中各个线程的操作;当线程终止时,条目将被静默地从你的地图中删除,并且如果你是最后一个引用它的对象,则不会阻止Thread被垃圾回收。然后,你可以迭代这个Map中的条目,了解有关系统中活动线程的元数据。
有关更多信息,请参见WeakHashMap不是缓存!
对于你所需要的缓存类型,可以使用专门的缓存系统 (例如 EHCache),或者查看 Guava 的 MapMaker 类 。例如:
new MapMaker().weakValues().makeMap();
这将实现您想要的功能,或者如果您想变得更加高级,可以添加定时过期:
new MapMaker().weakValues().expiration(5, TimeUnit.MINUTES).makeMap();
WeakHashMap
的主要用途是当你希望某些映射随着它们的键消失时也消失。而缓存则相反,当值消失时,你希望映射也会消失。
对于缓存来说,你需要一个 Map<K,SoftReference<V>>
。一个SoftReference
在内存紧张时会被垃圾回收掉(与WeakReference
相反,后者可能在其引用物不再存在硬引用时立即被清除)。在缓存中,你希望你的引用是软引用(至少在 key-value 映射不会过期的情况下),这样如果以后要查找这些值,则这些值仍有可能存在于缓存中。如果使用的是弱引用,那么你的值将立即被垃圾回收掉,从而打败了缓存的目的。
为方便起见,你可能希望隐藏你的 Map
实现中的 SoftReference
值,使得你的缓存看起来像是类型为 <K,V>
而不是 <K,SoftReference<V>>
。如果你想这样做,这个问题中有一些可以在网络上找到的实现建议。
还要注意的是,当你在 Map
中使用 SoftReference
值时,你必须手动删除已经清除了其 SoftReferences
的键值对,否则你的 Map
将永远增长,并泄漏内存。
Map<K, SoftRereference<?>>
的方法会在 GC 运行后在地图中留下包含 null
引用的 SoftReference
实例。我认为这个映射的内部实现必须定期清除所有值为软引用且持有 null
引用的映射,以进行良好的清理。 - TimmosHashMap<K, SoftReference<V>>
这种类型的实现,那么这会导致内存泄漏。你可能需要在回答中提到这一点。看看WeakHashMap
是如何处理它的,在Oracle JDK中有一个名为expungeStaleEntries
的私有方法来负责清理工作。 - Timmosimport 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'd的Weak References的条目的映射。
Map<K, V>
,而不是Map<K, WeakReference<V>>
。这个答案表面上看起来很有道理,但请注意,每次用户调用Map.get
时,缺失的映射都可以被删除,而且由于这正是WeakHashMap删除键的方式,Java团队不可能没有意识到这一点。 - Pacerierget
方法并且发现WeakReference
的引用为null时,会删除该条目。这仍然容易受到内存泄漏的影响,具体取决于使用方式,因为它依赖于对特定键的get
调用。最好使用带有特定驱逐机制的缓存。 - Shannon如果你需要弱引用,那么它其实非常容易:
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