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


69

Java的WeakHashMap通常被认为对缓存有用。尽管其弱引用是根据地图的键而不是其值定义的,但这似乎很奇怪。我的意思是,这是我要缓存的值,而一旦缓存中没有其他人强烈引用它们,我想获取垃圾回收值,不是吗?

以哪种方式保留对键的弱引用?如果执行a ExpensiveObject o = weakHashMap.get("some_key"),那么我希望高速缓存保持在'o'状态,直到调用者不再拥有强引用为止,并且我根本不在乎字符串对象“ some_key”。

我想念什么吗?


1
Java API充满了奇怪的怪癖。您始终可以使用WeakReference重写WeakHashMap。
Pacerier

Answers:


121

WeakHashMap不能用作缓存,至少在大多数人看来是这样。如您所说,它使用的是弱而不是弱的,因此它并不是针对大多数人想要使用的功能而设计的(事实上,我见过人们不正确地使用它)。

WeakHashMap最有用的是保留有关您无法控制其生命周期的对象的元数据。例如,如果您有一堆通过类的对象,并且您希望跟踪有关它们的额外数据,而无需在它们超出范围时得到通知,并且无需引用它们就可以使它们保持活动状态。

一个简单的示例(以及我之前使用过的示例)可能类似于:

WeakHashMap<Thread, SomeMetaData>

您可以在哪里跟踪系统中的各个线程在做什么;当线程死亡时,该条目将从您的映射中静默删除,并且如果您最后引用该线程,则不会阻止该线程被垃圾回收。然后,您可以遍历该映射中的条目,以查找有关系统中活动线程的元数据。

在非缓存中查看WeakHashMap!欲获得更多信息。

对于您想要的缓存类型,请使用专用的缓存系统(例如EHCache)或查看GuavaMapMaker类;就像是

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

将执行您想要的操作,或者如果您想花哨的话,可以添加定时到期时间:

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

4
只是为了在2013年8月进行更新:Google收藏现在命名为Guava,并且缓存创建逻辑现在是CacheBuilder类的一部分。
马修·菲利普斯


1
请注意,我认为在您的MapMaker示例中,您原本应该说新的MapMaker()。softValues()。makeMap(),因为调用weakValues()会得到与WeakHashMap相同的结果。有一个很好的例子,这里如何建立与地图制作工具的高速缓存- stackoverflow.com/questions/3737140/...
jklp

1
jklp-不,weakValues()与WeakHashMap根本不一样。weakKeys()与弱哈希图相同。WeakHashMap拥有对其值的强引用!同意在大多数情况下,软性比软性更有意义;然而,这是一个度的问题(软引用可以,但不要求,只要不存在内存压力,围绕保持;弱引用会被扔掉一批GC周期)和语义是没有什么不同。
2014年

1
链接断开。
Christopher

39

主要用途WeakHashMap是当您具有要在其键消失时消失的映射。缓存是相反的-您具有要在其值消失时消失的映射。

对于缓存,您想要的是一个Map<K,SoftReference<V>>SoftReference内存不足时,A将被垃圾回收。(将此与进行对比WeakReference,一旦不再有对其硬引用就可以清除它。)您希望您的引用在缓存中保持软化(至少在键值映射不陈旧的缓存中) ),从那时起,如果以后再查找它们,则您的值就有可能仍在缓存中。如果引用比较弱,那么您的值将立即被gc'击败,从而达到缓存的目的。

为了方便起见,您可能希望隐藏实现中的SoftReferenceMap,以使缓存看起来是<K,V>而不是的类型<K,SoftReference<V>>。如果您想这样做,此问题对网络上的可用实现提出了建议。

还请注意,当您在中使用SoftReference值时Map,您必须执行一些操作以手动删除已SoftReferences清除了它们的键值对-否则,您的Map大小将永远增长,并泄漏内存。


1
随着时间的流逝使用此解决方案会给您带来许多哈希值已被gc-ed的哈希映射项。是否有使用类似方法的替代方法?
android开发人员

1
在GC运行后,一种Map<K, SoftRereference<?>>方法会SoftReference在包含null引用对象的映射中留下实例。我认为,此映射的内部实现必须定期清除所有映射,并使用一个值(该值是包含引用null对象的软引用)来进行良好的清理。
2015年

4
(续)严格来说,如果一个天真的程序员使用该实现,HashMap<K, SoftReference<V>>则将导致内存泄漏。您可能会考虑将其包含在您的答案中。看一下WeakHashMap它是如何进行的,Oracle JDK中有一个私有方法expungeStaleEntries来处理此清理工作。
2015年

@Timmos,您是说从obj实例到SoftReference <V>的内部JVM引用将阻止收集?然后,这是一个JVM错误,需要修复。
佩里耶

2
不可以,如果您没有采取行动从地图中删除这些SoftReferences的事实,只是SoftReference引用的GC值仍在映射中而没有任何目的的事实就是内存泄漏。SoftReferences中包含的值最终将进行GC处理,但SoftReferences本身不会。这是程序员的任务。
Timmos

7

要考虑的另一件事是,如果采用此Map<K, WeakReference<V>>方法,则值可能会消失,但映射不会消失。根据使用情况,您可能最终会得到一个包含许多条目的地图,这些条目的弱引用已被GC处理。


Map<K, V>不是Map<K, WeakReference<V>>。从表面上看,这个答案似乎是有道理的,但是请注意,每次用户调用时Map.get,都可以删除丢失的映射,并且看到这正是WeakHashMap删除键的方式,这不可能是Java团队并未意识到这一点。 。
Pacerier

7

您需要两个映射:一个映射在高速缓存键和弱引用值之间映射,一个映射在相反方向上在弱引用值和键之间映射。您需要一个参考队列和一个清理线程。

当被引用的对象无法再访问时,弱引用可以将其移入队列。此队列必须由清理线程清除。 为了进行清理,有必要获取密钥以供参考。 这就是为什么需要第二张地图的原因。

以下示例显示如何使用弱引用的哈希映射创建缓存。运行程序时,将得到以下输出:

$ javac -Xlint:未选中Cache.java && Java缓存
{偶数:[2,4,6],奇数:[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();
    }
}

2
好答案。值得一提的是,与许多其他种类的集合不同,ReferenceQueue会阻塞,直到可以从queue.remove()返回值为止。这意味着清理线程不是乍一看可能会建​​议的非等待无限循环。
克里斯·哈顿

@Gordon如果您使用弱键和弱值,则所有东西都是弱的,并且在将其添加到缓存后会立即进行垃圾收集。
2015年

这就是答案。因此,我们可以说,API的实现是为了优化清理阶段
Pacerier
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.