如果没有明确清理,HashMap<String, WeakReference<>>是否会潜在地导致内存泄漏?

3

我发现了一段代码,我认为它会导致意外的内存泄漏:

Object user = getUser("Bob");

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

这张地图的目的是为了缓存对象,并在它们不再被强引用时,由GC自动从地图中清除。然而,我认为如果键也不是弱引用,那么一旦对象被GC回收,哈希映射中仍将存在一个指向null的键值对。因此,地图仍然包含相同数量的行,只是所有行都指向null值。因此,在上面的示例中,一旦所有对user的强引用都被释放并且GC销毁了对象,地图中的条目将等同于:
map.put("Bob", null );

除非有清理例程刷新所有键的空值,否则我的映射将继续增长。
因此问题变成了如何解决?是否有一种可以自动刷新条目的映射构造可用于如果值被销毁?
我考虑过做这样的事情:
Object user = getUser("Bob");

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

但这似乎是一个非常有限的用例,我的键必须是从我的值中检索出来的对象。使用WeakHashMap时,我的键不能是字符串常量(即:"Bob"),否则就不会有其他引用它的地方,垃圾回收器将从我的映射中清除该对象。

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

3个回答

4

你说得对,引用的收集并不会移除映射,但是结果并不等同于

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实例存在,引用完全相同的字符串实例作为映射键不会产生额外开销。您仍然可以像User u = map.get("Bob");这样使用字符串常量作为查找键,因为它们的相等性仍然是根据String.hashCode()String.equals()来确定的。如果使用字符串常量"Bob"放置映射,则该映射通常至少会持久存在,直到包含常量的代码处于活动状态(以及在此期间使用相同文字的任何其他代码),很可能是整个应用程序。但是,使用与引用中存储的不同字符串实例作为键有何意义呢?
请注意,WeakHashMap也必须处理相同的问题,条目不会自动删除。它必须使用ReferenceQueue来发现引用对象何时被收集以从表中删除其关联条目。当您调用它的方法时,此清理工作会发生,因此当您不调用它的方法时,表不会得到清理,但由于每次插入都会发生此清理,因此您受到免受不断增长的情况的保护。

2
如果键不是弱引用,那么一旦对象被垃圾回收,哈希映射中仍会有一个键指向 null 的条目。
但如果键指向一个非空的 WeakReference,该 WeakReference 又指向一个 null 引用。
因此,哈希映射仍将包含相同数量的行,只是所有行都指向 null 值。
不,参见上文。
解决方案是使用一个 WeakHashMap>,它具有通过 ReferenceQueue 检测到弱键被收集的后台活动,并删除相应映射的功能。更好的方法是使用一个 WeakHashMap>。然而:
如果键是字符串字面量,则无法进行收集。
将其描述为内存泄漏有点过了。Map 本身就是内存泄漏。如果您不想引用您的对象,请勿将其存储在 Map 中。

为什么字符串文字不可回收?它们不是像其他遵循相同规则的任何对象一样吗?只因为字符串是不可变的,我不明白为什么它们会被特殊对待。 - Eric B.
1
@Eric B.:规范要求相同的字符串文字解析为同一实例,这是使用 intern 表实现的。虽然此表引用的字符串是可收集的,但由字面量引用的字符串实例的中间收集将不符合始终解析为同一实例的指定行为。因此,只有在收集了包含字面量的所有代码后,才能收集这些字符串,以使收集发生时无法检测到。对于应用程序类加载器加载的代码,这永远不会发生。 - Holger
@holger 谢谢。我刚看到你下面的回复时就意识到了这一点。如果我执行 map.put( new String("Bob"), new WeakReference(user)),是否可以规避字符串字面值问题,或者这被认为是同样的事情?考虑到我有意创建一个字符串对象,我希望它将其视为对象而不是字面值,但我不确定。 - Eric B.
1
@Eric B.:这将解决字符串字面量问题,但会产生新的问题。由于没有任何东西引用通过new String("Bob")创建的新字符串实例,因此它立即符合垃圾回收条件,因此WeakHashMap可能会在下次查询时删除映射。这正是“有意创建字符串对象”的含义;该实例与表示字面量的对象不同,并且与User实例引用的对象也不同。 - Holger

1

你的推理是正确的。

  1. Map<String,WeakReference<Object>> 用于在“挂起”键不会成为问题的情况下使用,因为它们比映射中存储的对象要小得多。在这种情况下,有许多键映射到已清空的弱引用并不会对系统的内存资源造成足够的负担,以至于你无法注意到。

  2. 您可以在希望键被垃圾回收但确实正确使用 String 常量的情况下使用 WeakHashMap<K,V>,这确实会使其失去作用。通常,您使用自定义键类型,该类型使用引用相等性覆盖 equals


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接