逃避垃圾回收器?

3

好的,这可能是一个新手问题,但是我还是想问一下。

能否“超越”垃圾回收器?

我之所以这样问是因为我有一个递归方法,在应用程序中某个事件期间运行了多次(每秒大约60次,而且事件持续时间不确定,甚至可能长达数分钟)。问题在于,在递归方法的每个循环中,我都会创建一个相当复杂的对象(TreeViewItem,具有我们项目所需的一些修改和另一个作为TreeViewItem DataContext的复杂对象),因此我的担心是这些TreeViewItems在递归方法运行时留在堆中,因此当垃圾回收器启动时,它们不会被清除;可能下一次递归方法开始时,它会堆积更多的TreeViewItem,并且垃圾回收器永远无法跟上。

问题在于我们有一个内存泄漏问题,我们正在寻找罪魁祸首。

非常感谢任何帮助。


通过“stack”,我们认为你指的是堆。 - Marc
是的,我纠正了那个。我总是把它们搞混。 - Carlo
1
你是否在方法之外的某个地方保留了这些项的引用,例如,你将它们保存在某种集合中吗?如果没有持久引用,GC 最终会清理它们。 - Ed S.
TreeViewList和它们的DataContext对象保留在TreeView中(我正在使用WPF)。当递归方法完成并重新开始(或项目刷新)时,我使用treeView.Items.Clear()清除集合并释放当前集合中所有项的引用(或者这样说),然后递归方法重新开始,所以根据这里的答案,我认为对象确实被清理了,可能是其他原因。 - Carlo
哦,我根本没有订阅事件。 - Carlo
感谢大家的帮助。经过一些测试,似乎问题不在这里,因为内存仍然在增加。我把问题交给了另一个程序员,他实现了我们认为正确的解决方案。再次感谢! - Carlo
8个回答

7

不,你不能通过过载垃圾回收器来造成内存泄漏。

实际上,垃圾回收器的工作方式使它能够很好地处理像你这样的情况。

如果您删除了对大量复杂对象的引用,垃圾回收器不会逐个收集对象。它会看到没有任何活动引用这些对象,然后一次性收集整个对象组。

大多数对象的生命周期很短,因此垃圾回收器被构建为能够高效处理这种情况。例如,如果您填满了第一代堆,并且有90%的对象需要收集,垃圾回收器不会删除90%的对象。相反,它将10%移至下一代并简单地清除第一代。

如果垃圾回收器仍然发现自己有很多工作要做,它就会去做,而您必须等待它完成。它在工作时会冻结您的线程,直到垃圾回收器完成工作为止。


我喜欢这个答案,非常详细。谢谢! - Carlo
这里有一个反例:http://flyingfrogblog.blogspot.co.uk/2010/10/can-you-repro-this-64-bit-net-gc-bug.html - J D

4
不,你不能“超越”垃圾收集器,如果必要的话它会中断你的代码。具体情况取决于你是否作为服务器运行。
更重要的问题是:你为什么认为你有内存泄漏?这就是你开始搜索原因的地方。
请不要调用GC.Collect(),它会带来更多的伤害。
附加1:
内存管理可能出现的主要“问题”是保留了你不再需要的引用。通常随着流程的自然进行,局部变量在方法返回时被清除。但你应该寻找对象之间建立的引用。一个显著的例子是事件处理程序,如果你的对象订阅了一个事件,那么该订阅包含对订阅对象的引用。确保在不再需要时清除这些引用。

1
每次运行此方法时,内存都会不断增加,发现此问题的程序员在半小时内达到1.5 GB后遇到了OutOfMemoryException。 - Carlo
我认为你也应该修订一下你的文章。你说你担心这些对象留在栈上,但它们是在堆上分配的,而不是栈上,我相信你的栈远远达不到1.5GB。 - Ed S.
1
你需要注意的一些事情是,在循环内部使用一次性对象而没有在之后处理它们(应该在 using 块中完成),以及在循环内执行大量字符串拼接。可能看到一些代码会有所帮助。 - Scott Dorman

1

非常抱歉不同意,但是确实有可能超越.Net的垃圾收集器,至少是并发版本(服务器显然是同步运行的)。

我曾在我的一个WinForms应用程序中看到过这种情况,该应用程序循环分配大量内存并在执行过程中删除引用。

我编写了一个简单的测试用例来证明这一点。在VB中:

while true
   dim foo(4500000) as byte ' allocate 4.5 MB
   foo = nothing ' c# null - remove ref
end while

在我的应用程序中(必须分配了许多对象,如您可以在Process Explorer中看到GC线程正在努力工作),最终会发生OutOfMemory异常。在空白的虚拟应用程序中,这将无法导致异常,因为GC可能能够非常快速地完成其工作。您可以调整内存块的大小以使其最终失败。
编辑:显然,如果您想要重现上面的内容,则需要以x86而不是x64运行,否则可能需要等待一段时间...

1

垃圾收集器会查看堆栈吗?

我使用的那些只关心堆,如果必要的话可以在移动堆时停止所有线程。

你有什么证据表明你有泄漏而不仅仅是一般性的太多东西?有时堆和栈增长的方向是相反的 - 内存不足并不意味着“泄漏”。


是的,这个问题听起来像是内存不足,而不是任何垃圾回收问题。让我想起了我的奥赛罗人工智能... - user142350
这些对象在GC堆中,但它们的根在堆栈上。由于它们是活动的,所以无法进行垃圾回收。 - Michael

0

我不相信是这样的;垃圾回收器通常与内存分配隐式地绑定在一起,因此可以在分配内存时调用;但是我不是 .Net 框架垃圾回收方面的专家。

(托管代码中)内存泄漏最有可能的原因是您已经完成但仍然具有引用的对象。


0

只有在检测到需要运行时,GC才会运行。换句话说,当它检测到有足够的内存压力需要进行收集周期时,通常(虽然这不是唯一的原因)是指没有足够的堆空间来执行下一个分配时。

你需要注意的一些事情是,在循环内使用可回收对象而没有在之后处置它们(应该在using块中完成),以及在循环内执行大量字符串连接。

你需要确保正确结束递归并返回调用栈。此外,你是否需要为每个TreeViewItem设置DataContext?这是可以在TreeView上设置一次,让TreeViewItems遍历层次结构找到适当的DataContext吗?


对不起,它是WPF对象,代表TreeView中的树节点。 - Carlo

0
如果您遇到以下问题:
每次运行此方法时,内存不断增加,发现此问题的程序员在半小时内达到1.5 GB后就会出现OutOfMemoryException。
那么我建议您逐层从循环中剥离函数(按照有意义的层级),直到找到不会丢失内存的部分。然后再逐渐添加函数,直到确定引起泄漏的函数。

0

你无法“超越”垃圾收集器,但可能会对其造成很大的压力。通常表现为高CPU使用率,而不是GC没有清理需要清理的大块内存。

如果你在半个小时内积累了OutOfMemoryException,则肯定不是你过载了垃圾收集器的情况。正如其他人所说,更有可能是你正在创建的新对象的引用在应用程序运行时仍然存在。这并不意味着你应该只是将计时器循环设置为将所有内容设置为null,你需要确定保留引用的原因及你目前未执行的工作结束后应采取的行动。

这种工作的最佳工具是内存分析器。它们将分析您应用程序中的活动对象及与之相关的引用,并允许您查找仍然存活的您认为应该被收集的对象以及原因。这将是您泄漏的原因。

GC(垃圾回收器)会识别应用程序中保证存活的对象(gc根),例如静态变量、局部变量等。然后,它将跟随根引用以查找每个当前存活的对象。其余的都是垃圾。这是一个非常简化的表述方式,但它显示了您需要找到的是哪些根路径导致您创建的对象,并在适当的时候打破它们。

虽然您说您自己没有使用事件,但很可能某个始终存在的控件正在订阅您的新对象上的事件,或者以其他方式将它们保存在某个列表中。

我以前经常使用Scitech memory profiler,发现它非常有帮助。还有其他工具-dotTrace, Ants memory profiler等等。我使用过其中的一些,发现Scitech的分析器最好。它们可能都有试用期! :)


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