另一种可能造成巨大内存泄漏的方法是保存对 Map.Entry<K,V> 的引用,而不是 TreeMap。
很难评估为什么这仅适用于TreeMaps,但通过查看实现,原因可能是:TreeMap.Entry 存储对其兄弟的引用,因此如果 TreeMap 准备好已收集,但其他一些类持有对其任何 Map.Entry 的引用,则 整个 Map 将被保留到内存中。
真实场景:
想象一下,有一个 db 查询返回一个大的 TreeMap 数据结构。人们通常使用TreeMaps 作为保留元素插入顺序。
public static Map<String, Integer> pseudoQueryDatabase();
如果查询被多次调用,并且对于每个查询(因此,对于每个返回的 Map),您在某处保存一个 Entry,内存将不断增长。
考虑以下包装类:
class EntryHolder {
Map.Entry<String, Integer> entry;
EntryHolder(Map.Entry<String, Integer> entry) {
this.entry = entry;
}
}
应用:
public class LeakTest {
private final List<EntryHolder> holdersCache = new ArrayList<>();
private static final int MAP_SIZE = 100_000;
public void run() {
// create 500 entries each holding a reference to an Entry of a TreeMap
IntStream.range(0, 500).forEach(value -> {
// create map
final Map<String, Integer> map = pseudoQueryDatabase();
final int index = new Random().nextInt(MAP_SIZE);
// get random entry from map
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue().equals(index)) {
holdersCache.add(new EntryHolder(entry));
break;
}
}
// to observe behavior in visualvm
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
public static Map<String, Integer> pseudoQueryDatabase() {
final Map<String, Integer> map = new TreeMap<>();
IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
return map;
}
public static void main(String[] args) throws Exception {
new LeakTest().run();
}
}
在每次pseudoQueryDatabase() 调用之后,map 实例应该准备好收集,但不会发生,因为至少有一个 Entry 存储在其他地方。
根据您的jvm 设置,应用程序可能会在早期由于OutOfMemoryError 而崩溃。
您可以从这张visualvm 图表中看到内存是如何不断增长的。
哈希数据结构 (HashMap) 不会发生同样的情况。
这是使用HashMap时的图表。
解决方案?直接保存键/值(您可能已经这样做了)而不是保存Map.Entry。
我写了一个更广泛的基准here。