【问题标题】:Why WeakHashMap holds strong reference to value after GC?为什么 WeakHashMap 在 GC 后持有强引用值?
【发布时间】:2016-01-19 13:32:23
【问题描述】:

WeakHashMap 中的关键对象变得弱可达。并且 map 应该在 GC 之后删除条目。但是对值对象的强引用仍然存在。为什么?

使用 guava 弱键映射观察到相同的行为。

预期输出:

...
refKey.get = null
refValue.get = null

但我得到了输出:

map.keys = []
map.values = []
map.size = 0
refKey.get = null
refValue.get = (123)

代码:

import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;
import com.google.common.collect.MapMaker;

public class Test {

    static class Number {
        final int number;
        public Number(int number) { this.number = number; }
        public String toString() { return "(" + number + ")"; }
    }

    static class Key extends Number {
        public Key(int number) { super(number); }
    }

    static class Value extends Number {
        public Value(int number) { super(number); }
    }

    public static void main(String args[]) {

        //Map<Key, Value> map = new MapMaker().weakKeys().makeMap();
        Map<Key, Value> map = new WeakHashMap<>();

        Key key = new Key(1);
        Value value = new Value(123);

        map.put(key, value);

        WeakReference<Key> refKey = new WeakReference<>(key);
        WeakReference<Value> refValue = new WeakReference<>(value);

        key = null;
        value = null;

        System.gc();

        System.out.println("map.keys = " + map.keySet());
        System.out.println("map.values = " + map.values());
        System.out.println("map.size = " + map.size());
        System.out.println("refKey.get = " + refKey.get());
        System.out.println("refValue.get = " + refValue.get());

    }

}

UPD:

我尝试在 jСonsole 和 jcmd 中执行 GC,但输出没有改变。

【问题讨论】:

  • @Thilo 在这种情况下,我会得到输出:refKey.get() = (1)。但是refKey.get() = null.
  • 但是当垃圾收集只完成一半时,你可以获得一个输出,不是吗?
  • 您是否尝试使用 jconsole 或 jcmd 来查看是否会改变输出(您必须睡足够长的时间才能适应它)。
  • @Thilo 感谢您的建议。我尝试了 jConsole 和 jcmd,但输出没有改变。
  • @Thilo 尝试了一百个键和值。收集了所有键,但收集了所有值 - 没有。

标签: java garbage-collection guava weakhashmap


【解决方案1】:

WeakHashMap 包含使用 WeakReference 引用密钥的 Map.Entry 实例(实际上,在 OpenJDK / Oracle JDK 中,it directly extends WeakReference)。

当 GC 发生时,现在引用缺失键的条目不会从映射中神奇地删除:它们仍然存在直到被清除,这就是为什么值仍然存在并且尚未被收集的原因。

在 OpenJDK 中,这发生在使用 ReferenceQueueexpungeStaleEntries() 中,并且从多个位置调用该方法:

  • size()
  • resize()
  • getTable() 本身被多种方法调用,包括get()put()

如果您希望您的值可被垃圾回收,您应该与WeakHashMap 进行交互,例如通过询问其size() 或进行查找。

请注意,这意味着在 第二次 垃圾收集之前无法收集该值。

如果我没记错的话,它在 Guava 中的工作方式或多或少是相同的。

【讨论】:

  • 这有点蹩脚......应该在 JavaDocs 中提到(类似于他们关于从值中间接引用键的警告)。
  • 回复:“跛脚”。 OTOH,这在现实生活中可能根本不是问题,因为每当您对地图执行任何操作时都会发生清除。仍然......可能已经在 J​​avadocs 中提及。
  • @Thilo 是的,对于不熟悉垃圾收集过程的人来说,它可能在 Javadoc 中更加明确。如果您熟悉它,您就会知道如果不与地图进行一些交互,条目就不会神奇地消失。在MapMaker 的 Javadoc 中它稍微更明确一些,但也不完全。
【解决方案2】:

在 Jconsole 中运行此代码修改并强制 GC 确认删除引用。

    System.gc();

    try {
        while (refValue.get() != null) {
            System.out.println("map.keys = " + map.keySet());
            Thread.sleep(5000);
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("map.keys = " + map.keySet());
    System.out.println("map.values = " + map.values());
    System.out.println("map.size = " + map.size());
    System.out.println("refKey.get = " + refKey.get());
    System.out.println("refValue.get = " + refValue.get());

但由于某种原因,如果map.keySet() 没有在循环中被查询,它永远不会终止,即refValue 不会变成null

【讨论】:

  • 如果你查看 WeakHashMap 的源代码,有一个方法 expungeStaleEntries 当你请求 sizekeySet 时会被调用。大概这摆脱了对值的引用(在此之前,存在对它的硬引用)。
【解决方案3】:

您无法控制 java 中的 GC。虚拟机决定。我从来没有遇到过需要 System.gc() 的情况。由于 System.gc() 调用只是简单地建议 VM 进行垃圾收集并且它还进行完整的垃圾收集(多代堆中的旧代和新代),因此它实际上可能导致消耗更多的 cpu 周期必要的。

在 jdk 1.7 以后,您可以使用以下命令强制 gc 运行

jcmd <pid> GC.run

使用jps获取进程ID

【讨论】:

  • 这没有回答问题。
  • n jdk 1.7 以后可以使用以下命令强制 gc 运行 jcmd GC.run 您可以使用 jps 获取进程 ID。
猜你喜欢
  • 2012-02-28
  • 2020-04-05
  • 2011-05-03
  • 1970-01-01
  • 2010-12-20
  • 1970-01-01
  • 2014-07-04
  • 2013-02-22
  • 1970-01-01
相关资源
最近更新 更多