从静态对象中释放C#字典的内存

19
我遇到了一些WCF Web服务的问题(一些转储,内存泄漏等),并运行了一个性能分析工具(ANTS Memory Profiles)。

结果发现即使处理已经结束(我运行了一个特定的测试然后停止了),第二代内存仍占Web服务内存的25%。我追踪这个内存,发现我有一个字典对象,其中包含(null,null)项,哈希码为-1。

Web服务的工作流程意味着在特定的处理过程中添加和删除项目到字典中(只是简单的AddRemove)。这不是什么大问题。但似乎在所有项目被删除后,字典中仍充满了(null,null)KeyValuePair。实际上有成千上万个,它们占据了大量内存,最终会导致溢出,强制应用程序池回收并使DW20.exe获取所有可获取的CPU周期。

实际上,该字典是 Dictionary<SomeKeyType, IEnumerable<KeyValuePair<SomeOtherKeyType, SomeCustomType>>> (由于大字典导致System.OutOfMemoryException),因此我已经检查是否有某种引用来保存内容。

该字典包含在静态对象中(以便通过处理使其可访问不同的处理线程),因此根据这个问题和更多问题(静态成员会被垃圾回收吗?),我理解为什么该字典在第2代。但这也是那些(null,null)的原因吗?即使我从字典中删除项目,某些东西也将一直占用内存吗?

这不像这个问题中的速度问题 从C#中的大数据结构中释放内存。似乎内存永远不会被回收。

我能做些什么来真正从字典中删除项,而不仅仅是用(null,null)对填充它?还有其他需要检查的吗?


1
+!:为了进行适当的研究并撰写一个好问题。 - Daniel A.A. Pelsmaeker
1
List<>有一个TrimExcess()方法,Dictionary<,>不幸地没有。我以为这很容易得分 :) - C.Evenhuis
调用Dictionary.Clear()有什么区别吗? - Alex
@C.Evenhuis:我将用ListTrimExcess()来换取Dictionary的性能,但这本来是个好主意。尽管其他人似乎对此感兴趣:https://dev59.com/dnI-5IYBdhLWcg3w483k - Coral Doe
@Alex:我已经尝试调用Clear()并检查操作是否真的发生,但它并没有改变所描述的行为。 - Coral Doe
3个回答

17
Dictionaries将项目存储在哈希表中。内部使用数组实现。由于哈希表的工作方式,此数组必须始终大于实际存储的项目数量(至少大约30%)。Microsoft使用72%的负载因子,即数组的至少28%将为空(请参见使用C#2.0对数据结构进行广泛研究,特别是System.Collections.Hashtable类System.Collections.Generic.Dictionary类)。因此,null / null条目只表示此空闲空间。

如果数组太小,则它将自动增长;但是,当删除项目时,数组不会收缩,但应重用将被释放的空间以插入新项目。

如果您掌握这个字典的控制权,您可以尝试重新创建它以缩小它的大小:

theDict = new Dictionary<TKey, IEnumerable<KeyValuePair<TKey2, TVal>>>(theDict);

但问题可能出现在实际(非空)条目上。您的字典是静态的,因此除非您将其分配给另一个字典或nulltheDict = new ...theDict = null),否则垃圾回收器永远不会自动回收它。这仅适用于字典本身是静态的,而不适用于其条目。只要对已删除条目的引用存在于其他地方,它们就会持续存在。GC将回收任何无法通过某些引用再次访问的对象(早期或晚期)。无论此对象是否声明为静态,都没有区别。对象本身不是静态的,只有它们的引用是。


自从 .NET Core 2.1 版本之后,@RobertTausig 友好地指出,有了新的 Dictionary.TrimExcess() 方法,这正是您所需要的,但当时不存在。

谢谢您提供的信息,我现在更加清楚了。虽然我看到了可用空闲内存的优点,但我仍然想按照您和@usr建议的方式来“收缩”它。我只需要更好地分析何时需要这样做。 - Coral Doe
@Olivier 你所说的不会在删除项目时收缩,是指内存仍然会被使用吗? - sm_
@SirajMansour,删除项不会改变哈希表数组的大小。值类型的键和值直接存储在该数组中。对于引用类型,仅存储引用(或在删除项目后为null)在数组中。如果这些引用所指向的对象内存没有被其他地方引用,它们可以被垃圾收集器回收。 - Olivier Jacot-Descombes

5

看起来您需要定期清理字典中的空间。您可以通过创建一个新的字典实现: new Dictionary<a,b>(oldDict)。请确保以线程安全的方式进行此操作。

什么时候这样做?可以在定时器的滴答声(60秒?)或发生特定数量的写入(100k?)时执行(您需要保持修改计数器)。


0
一个解决方案是在静态字典上调用Clear()方法。这样,字典的引用仍然可用,但包含的对象将被释放。

有人在评论中提到过这个问题,当时我回复说使用Clear方法并不能达到我想要的效果。 - Coral Doe

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