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

Mat*_*ias 63 java caching weak-references

Java的WeakHashMap经常被认为对缓存很有用.虽然它的弱引用是根据地图的键而不是它的值来定义的,但这似乎很奇怪.我的意思是,这是我想要缓存的值,除了缓存之外没有其他人强烈引用它们,我想要收集垃圾,不是吗?

以哪种方式有助于保持对密钥的弱引用?如果你这样做ExpensiveObject o = weakHashMap.get("some_key"),那么我希望缓存保持为'o',直到调用者不再持有强引用,并且我根本不关心字符串对象"some_key".

我错过了什么吗?

Cow*_*wan 109

WeakHashMap 作为缓存并不实用,至少大多数人都会想到它.正如你所说,它使用弱,而不是弱,所以它不是为大多数人想要使用它而设计的(实际上,我看到人们使用它,不正确).

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

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

WeakHashMap<Thread, SomeMetaData>
Run Code Online (Sandbox Code Playgroud)

您可以在哪里跟踪系统中的各种线程正在做什么; 当线程死亡时,该条目将从您的地图中静默删除,如果您是最后一次引用该线程,则不会使该线程被垃圾收集.然后,您可以遍历该映射中的条目,以找出有关系统中活动线程的元数据.

请参阅WeakHashMap而不是缓存!欲获得更多信息.

对于你所使用的缓存类型,要么使用专用缓存系统(例如EHCache),要么使用google-collections的 MapMaker类 ; 就像是

new MapMaker().weakValues().makeMap();
Run Code Online (Sandbox Code Playgroud)

会做你想做的事,或者如果你想得到想象,你可以添加定时到期:

new MapMaker().weakValues().expiration(5, TimeUnit.MINUTES).makeMap();
Run Code Online (Sandbox Code Playgroud)

  • 只是为了更新2013年8月的这个:Google Collections现在名为Guava,缓存创建逻辑现在是[CacheBuilder]的一部分(http://docs.guava-libraries.googlecode.com/git-history/release/javadoc /index.html)类. (4认同)
  • 链接已损坏。 (2认同)

uck*_*man 35

主要用途WeakHashMap是当你的键映射消失时你想要消失的映射.缓存是相反的 - 当你的值消失时,你想要消失的映射.

对于缓存,你想要的是一个Map<K,SoftReference<V>>.SoftReference当内存变紧时,A 将被垃圾收集.(将其与a进行对比WeakReference,只要不再对其指示对象进行硬引用,就可以将其清除.)您希望引用在缓存中是软的(至少在键值映射不会过时的情况下) ),从那时起,如果您以后查找它们,您的值仍有可能仍在缓存中.如果引用很弱,那么你的值将立即被gc'd,从而破坏了缓存的目的.

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

另请注意,当您SoftReference在a中使用值时Map,必须执行某些操作以手动删除已清除的键值对SoftReferences- 否则您的内容Map将永远增大,并且内存泄漏.

  • (继续)严格地说,如果一个天真的程序员使用实现`HashMap <K,SoftReference <V >>`那么这将导致内存泄漏.您可以考虑将此包含在您的答案中.看看`WeakHashMap`是如何做到的,Oracle JDK中有一个私有方法`expungeStaleEntries`来处理这个清理工作. (4认同)
  • 随着时间的推移,使用此解决方案会给您留下许多值已被 gc-ed 的哈希映射项。有没有使用类似方法的替代方案? (2认同)
  • GC 运行后,“Map&lt;K, SoftRereference&lt;?&gt;&gt;”方法会在包含“null”引用的映射中留下“SoftReference”的实例。我认为这个映射的内部实现必须定期清除所有具有值的映射,该值是持有“null”引用的软引用,以进行良好的清理。 (2认同)
  • 不行,如果您没有采取行动从地图中删除这些SoftReference的事实,则只是SoftReference引用的GC值仍在映射中而没有任何目的的事实就是内存泄漏。SoftReferences中包含的值最终将进行GC处理,但SoftReferences本身不会。这是程序员的任务。 (2认同)

Sha*_*non 7

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


cev*_*ing 6

您需要两个映射:一个映射在缓存键和弱引用值之间,另一个映射在弱引用值和键之间的相反方向映射.而且你需要一个引用队列和一个清理线程.

当引用的对象不能再被访问时,弱引用能够将引用移动到队列中.该队列必须由清理线程排干. 对于清理,有必要获取参考密钥. 这就是为什么需要第二张地图的原因.

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

$ 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 很好的答案。值得注意的是,与许多其他类型的集合不同,ReferenceQueue 会阻塞,直到可以从 queue.remove() 返回值为止。这意味着清理线程不是乍一看可能暗示的非等待无限循环。 (2认同)