【问题标题】:Will a HashMap<String, WeakReference<>> potentially cause a memory leak if not explicitly cleaned up?如果没有明确清理,HashMap<String, WeakReference<>> 是否会导致内存泄漏?
【发布时间】:2017-09-15 02:04:22
【问题描述】:

我遇到了一段我确信会导致意外内存泄漏的代码:

Object user = getUser("Bob");

Map<String, WeakReference<Object>> map = new HashMap<>();
map.put("Bob", new WeakReference( user ) );

此映射的目的是缓存对象,并在它们不再被强引用时由 GC 从映射中自动清除。

但是,我看到的方式是,如果键也不是弱引用,那么一旦对象被 GC'ed,哈希映射中仍然会有一个键指向 null 的条目。因此,地图仍将包含相同数量的行,只是都指向空值。

所以在上面的例子中,一旦对user的所有强引用被释放,并且GC销毁Object,映射中的条目将相当于:

map.put("Bob", null );

因此,除非有一个清理例程来刷新所有具有空值的键,否则我的地图将继续增长。

那么问题就变成了如何解决这个问题?是否有一个我可以使用的地图构造,如果值被破坏,它会自动刷新我的条目?

我打算做类似的事情:

Object user = getUser("Bob");

Map<String, WeakReference<Object>> map = new WeakHashMap<>();
map.put(user.getUsername(), new WeakReference( user ) );

但这似乎是一个非常有限的用例,我的密钥必须是从我的值中检索的对象。使用WeakHashMap,我的键不能是字符串常量(即:“Bob”),否则不会有任何其他对它的引用,GC 将从我的地图中清除该对象。

是否有其他缓存结构可以提供所有这些功能?

【问题讨论】:

    标签: java memory-leaks garbage-collection weak-references


    【解决方案1】:

    你是对的,引用对象的集合并没有删除映射,但是结果不等于

    map.put("Bob", null );
    

    相当于

    map.put("Bob", new WeakReference<>(null) );
    

    所以你不仅有一个悬空条目实例,还有一个悬空清除 WeakReference 实例。

    当你使用

    Map<String, WeakReference<User>> map = new WeakHashMap<>();
    User user = getUser("Bob");
    map.put(user.getUsername(), new WeakReference( user ) );
    

    您将获得所需的语义,假设 user.getUsername() 返回对存储在 User 对象中的字符串实例的引用,以确保只要 User 是强可达的,它就保持强可达。

    我认为这里没有任何限制。由于存储在User 中的字符串实例确实存在,只要User 实例存在,引用与映射键完全相同的字符串实例就没有开销。您仍然可以使用字符串常量作为查找键 ala User u = map.get("Bob");,因为它们的相等性仍然由 String.hashCode()String.equals() 确定。如果您put 使用字符串常量"Bob" 进行映射,则映射通常至少会持续存在,至少只要包含该常量的 code 还存在(以及在此期间使用相同文字的其他代码)生命周期),可能是整个应用程序。但是使用不同的字符串实例作为键而不是存储在所指对象中的意义在哪里?

    请注意WeakHashMap 必须处理相同的问题,条目不会自动删除。它必须使用ReferenceQueue 来发现何时收集了所指对象以从表中删除其关联条目。 This cleanup 每当您在其上调用方法时都会发生,因此当您不在其上调用方法时,表不会被清理,但由于每次插入都会发生这种清理,因此您可以免受不断增长的情况的影响。

    【讨论】:

      【解决方案2】:
      1. 如果 key 也不是弱引用,那么一旦 Object 被 GC'ed,hash map 中仍然会有一个 key 指向 null 的条目。

      键指向一个非空的WeakReference,该WeakReference指向一个空的引用对象。

      因此地图仍将包含相同数量的行,只是都指向空值。

      不,见上文。

      解决方案是使用WeakHashMap&lt;String, WeakReference&lt;Object&gt;&gt;,它具有通过ReferenceQueue 发现正在收集的弱密钥的后台活动,并删除相应的映射。更好的是,WeakHashMap&lt;String, WeakReference&lt;User&gt;&gt;。然而:

      1. 如果键是字符串字面量,则它们无论如何都无法收集。

      将其描述为内存泄漏有点丰富。 Map 本身就是内存泄漏。如果您不想引用您的对象,请不要将它们存储在 Map 中。

      【讨论】:

      • 为什么字符串文字不可收集?它们不只是像任何其他遵循相同规则的对象吗?仅仅因为字符串是不可变的,我不明白为什么它们会被区别对待。
      • @Eric B.:规范要求将相同的字符串文字解析为相同的实例,这是使用实习表实现的。虽然此表引用的字符串是可收集的,但检测到的由文字引用的字符串实例的中间集合与始终解析为同一实例的指定行为不匹配。因此,只有在也收集了包含文字的所有代码时才可能收集此类字符串,以使收集发生时无法检测到。对于由应用程序类加载器加载的代码,这永远不会发生。
      • @holger 谢谢。当我在下面阅读您的回复时,我意识到这一点。做map.put( new String("Bob"), new WeakReference(user)) 会绕过字符串文字问题,还是被认为是同一件事?鉴于我有意创建一个字符串对象,我希望它会这样对待它而不是文字,但我不确定。
      • @Eric B.:它会解决字符串文字问题,但会创建一个新问题。由于没有引用通过new String("Bob") 创建的新字符串实例,因此它可以立即进行垃圾回收,因此WeakHashMap 可以在您下次查询时删除映射权。这正是“故意创建字符串对象”的含义;该实例不同于表示字面量的对象,也不同于 User 实例引用的对象。
      【解决方案3】:

      你的推理是正确的。

      1. Map&lt;String,WeakReference&lt;Object&gt;&gt; 用于“延迟”键不存在问题的情况,因为它们比存储在映射中的对象小很多。在这种情况下,将大量键映射到清空的弱引用不会使系统的内存资源紧张到足以让您注意到。

      2. 您可以使用WeakHashMap&lt;K,V&gt; 来处理您希望垃圾回收密钥的情况,但您绝对正确地使用String 常量:这确实会破坏目的。通常,您使用一个自定义键类型,该类型通过引用相等性覆盖 equals

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-04-14
        • 1970-01-01
        • 2017-12-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多