【问题标题】:Java's WeakHashMap and caching: Why is it referencing the keys, not the values?Java 的 WeakHashMap 和缓存:为什么它引用键而不是值?
【发布时间】:2010-12-20 15:25:34
【问题描述】:

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

在哪些方面有助于保持对键的弱引用?如果您执行ExpensiveObject o = weakHashMap.get("some_key"),那么我希望缓存保持为'o',直到调用者不再持有强引用,并且我根本不关心字符串对象“some_key”。

我错过了什么吗?

【问题讨论】:

  • Java API 充满了奇怪的怪癖。你总是可以使用 Wea​​kReference 重写 WeakHashMap。

标签: java caching weak-references


【解决方案1】:

WeakHashMap 没有作为缓存有用,至少大多数人认为它是这样。正如你所说,它使用弱 keys,而不是弱 values,所以它不是为大多数人想要使用它而设计的(事实上,我已经 见过人们错误地使用它)。

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

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

WeakHashMap<Thread, SomeMetaData>

您可以在其中跟踪系统中的各个线程正在执行的操作;当线程终止时,该条目将从您的地图中静默删除,如果您是对它的最后引用,您将不会阻止该线程被垃圾收集。然后,您可以遍历该映射中的条目,以找出您拥有的有关系统中活动线程的元数据。

更多信息请参见WeakHashMap in not a cache!

对于您所追求的缓存类型,请使用专用缓存系统(例如EHCache)或查看GuavaMapMaker class;像

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

会做你所追求的,或者如果你想变得花哨,你可以添加定时过期:

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

【讨论】:

  • 仅在 2013 年 8 月更新此内容:Google Collections 现在被命名为 Guava,缓存创建逻辑现在是 CacheBuilder 类的一部分。
  • 注意,我认为在您的 MapMaker 示例中,您应该说 new MapMaker().softValues().makeMap(),因为调用 weakValues() 会得到与 WeakHashMap 相同的结果。这里有一个很好的例子来说明如何使用 MapMaker 构建缓存 - stackoverflow.com/questions/3737140/…
  • jklp - 不,weakValues() 与 WeakHashMap 完全不同。 weakKeys() 与弱哈希映射相同。 WeakHashMap 持有对其值的强引用!同意在大多数情况下,软比软更有意义;然而,这是一个程度问题(软引用可能,但不是要求,只要没有内存压力就一直保留;弱引用将在下一个 GC 周期被丢弃)并且语义没有什么不同。
  • 链接已损坏。
【解决方案2】:

WeakHashMap 的主要用途是当您有映射时,您想在它们的键消失时消失。缓存是相反的——当它们的值消失时,你有想要消失的映射。

对于缓存,您需要的是Map&lt;K,SoftReference&lt;V&gt;&gt;。当内存紧张时,SoftReference 将被垃圾收集。 (将其与WeakReference 进行对比,一旦不再有对其所指对象的硬引用,它就可能被清除。)您希望您的引用在缓存中是软的(至少在键值映射不存在的地方) t 过时),从那时起,如果您稍后查找它们,您的值仍有可能在缓存中。相反,如果引用很弱,您的值将立即被 gc'd,从而破坏了缓存的目的。

为方便起见,您可能希望在您的Map 实现中隐藏SoftReference 值,以便您的缓存看起来是&lt;K,V&gt; 类型而不是&lt;K,SoftReference&lt;V&gt;&gt;。如果你想这样做,this question 有网络上可用的实现建议。

还请注意,当您在 Map 中使用 SoftReference 值时,您必须手动删除已清除其 SoftReferences 的键值对---否则您的Map 只会永远变大,而且会泄漏内存。

【讨论】:

  • 随着时间的推移使用此解决方案会给您留下许多值已被 gc-ed 的哈希图项。有没有使用类似方法的替代方法?
  • Map&lt;K, SoftRereference&lt;?&gt;&gt; 方法在 GC 运行后在包含 null 引用的映射中留下 SoftReference 的实例。我认为这个映射的内部实现必须定期清除所有映射,其值是一个软引用,持有一个null 引用,以便进行良好的清理。
  • (续)严格来说,如果一个天真的程序员使用实现HashMap&lt;K, SoftReference&lt;V&gt;&gt;,那么这将导致内存泄漏。您可以考虑将其包含在您的答案中。看看WeakHashMap 是怎么做的,Oracle JDK 中有一个私有方法expungeStaleEntries 负责这个清理工作。
  • @Timmos,你的意思是从 obj 实例到 SoftReference 的内部 JVM 引用会阻止收集吗? 那就是需要修复的JVM bug。
  • 不,如果您未能采取措施从映射中删除这些 SoftReference,那么 SoftReference 引用的 GC 值仍然在映射中没有任何目的的事实就是内存泄漏。 SoftReferences 中包含的值最终会被 GC,但 SoftReferences 本身不会。这是程序员的任务。
【解决方案3】:

要考虑的另一件事是,如果您采用Map&lt;K, WeakReference&lt;V&gt;&gt; 方法,该值可能会消失,但映射不会。根据使用情况,您最终可能会得到一个包含许多弱引用已被 GC'd 的条目的 Map。

【讨论】:

  • Map&lt;K, V&gt;,而不是Map&lt;K, WeakReference&lt;V&gt;&gt;。这个答案从表面上看似乎是有道理的,但请注意,每次用户调用Map.get 时,都可以删除丢失的映射,看到this is exactly how WeakHashMap removes keys,Java 团队不可能没有意识到这一点。
【解决方案4】:

您需要两个映射:一个在缓存键和weak referenced 值之间进行映射,另一个在弱引用值和键之间进行反向映射。你需要一个reference queue 和一个清理线程。

当被引用对象无法再访问时,弱引用能够将引用移动到队列中。该队列必须由清理线程清空。 为了进行清理,需要获取密钥以供参考。这就是需要第二张地图的原因。

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

$ javac -Xlint:unchecked Cache.java && java Cache {偶数:[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();
    }
}

【讨论】:

  • 很好的答案。值得注意的是,与许多其他类型的集合不同,ReferenceQueue 会阻塞,直到可以从 queue.remove() 返回值。这意味着清理线程并不是乍一看可能暗示的非等待无限循环。
  • @Gordon 如果你使用弱键和弱值,那么一切都是弱的,并且在你将它添加到缓存之后就会被垃圾回收。
  • 这就是答案。所以我们可以说,API 是为了优化清理阶段而实现的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-10-09
  • 1970-01-01
  • 2012-02-28
  • 1970-01-01
  • 2019-07-24
  • 1970-01-01
  • 2017-09-15
相关资源
最近更新 更多