C# .NET的垃圾回收功能失效了吗?

9
我正在使用Visual Studio 2010开发一个相对较大的解决方案。它有各种项目,其中之一是XNA游戏项目,另一个是ASP.NET MVC 2项目。
我在这两个项目中都遇到了同样的问题:在调试模式下启动后,内存使用量不断上升。它们最初的内存使用量分别为40MB和100MB,但很快就会迅速攀升到1.5GB(分别需要10和30分钟)。之后,有时会降回接近初始值,而有时则会抛出OutOfMemoryExceptions异常。
当然,这表明存在严重的内存泄漏问题,所以我最初尝试找出问题所在。在未能成功找到泄漏点后,我尝试每隔10秒钟调用一次GC.Collect()方法。引入这个“hack”后,内存使用量分别保持在45MB和120MB左右,可以持续24小时(直到我停止测试)。
.NET的垃圾回收机制应该是“非常好”的,但我还是怀疑它没有完成工作。我使用CLR Profiler来排除问题,它显示XNA项目似乎已经保存了很多我的确在使用的字节数组,但是引用应该已经被删除,因此应该被垃圾回收器收集。
同样地,当我定期调用GC.Collect()方法时,内存使用问题似乎已经解决了。有人知道是什么原因导致了高内存使用率吗?这可能与在调试模式下运行有关吗?

1
在发布模式下是否重复出现相同的问题? - pickypg
这些单独的字节数组有多大?它们可能已经被放入了大对象堆中,该堆不像其他堆那样经常被回收。系统是否存在内存压力? - agent-j
1
你在使用“内存使用量”时是指哪个值?虚拟大小?私有字节? - Sven
@agent-j LOH对象被收集,但GC不会碎片化LOH中的空闲空间,因此即使内存中没有太多对象,应用程序也可能遭受内存不足问题。 - Alex Netkachov
让我们确保在得出结论之前它们确实是大数组。除非你想深入了解“罢工之子”并查看它们位于哪个堆中。@AlexAtNet,关于碎片化的观点很好。我从未真正见过它,但我不得不寻找它一两次。 - agent-j
显示剩余4条评论
6个回答

16

在搜索泄漏时没有成功

再努力一点 =)

在托管语言中,内存泄漏可能很难追踪。我在Redgate ANTS内存分析器方面有良好的经验。它不是免费的,但他们提供了一个14天的完整功能试用期。它具有良好的用户界面,并向您显示内存分配位置,以及为什么这些对象保存在内存中。

正如Alex所说,事件处理程序是.NET应用程序中非常常见的内存泄漏来源。请考虑以下内容:

public static class SomeStaticClass
{
    public event EventHandler SomeEvent;
}

private class Foo
{
    public Foo()
    {
        SomeStaticClass.SomeEvent += MyHandler;
    }

    private void MyHandler( object sender, EventArgs ) { /* whatever */ }
}

我在这里使用了一个静态类,以使问题尽可能明显。假设在应用程序的生命周期内创建了许多Foo对象。每个Foo都订阅了静态类的SomeEvent事件。

Foo对象可能会在某个时候超出范围,但静态类通过事件处理程序委托保持对每个对象的引用。因此,它们将无限期地保持活动状态。在这种情况下,事件处理程序只需要“取消挂钩”。

...XNA项目似乎保存了我确实在使用的许多字节数组...

您可能会遇到LOH中的碎片化问题。如果您经常分配大型对象,则可能会导致此问题。这些对象的总大小可能比分配给运行时的总内存小得多,但由于碎片化,有很多未使用的内存分配给您的应用程序。

我上面提供的分析工具将告诉您是否存在此问题。如果是,则可能会追踪到某个对象泄漏。我刚刚解决了我的应用程序中显示相同行为的问题,原因是MemoryStream即使在调用Dispose()后也没有释放其内部的byte[]。将流包装在虚拟流中并将其设置为null即可解决问题。

此外,显而易见的是,请确保Dispose()实现了IDisposable接口的对象。可能会有未释放的本机资源。同样,一个好的分析工具可以捕获到这个问题。

我的建议是:问题不在于垃圾回收,而是在于你的应用程序。使用分析器,使你的应用程序处于高内存消耗状态,拍摄内存快照并开始分析。


1
谢谢,非常有帮助的帖子。我之前不知道事件处理程序会阻止对象被回收,我一定会深入研究一下。稍后我会发布结果。 - Jacotb
1
我按照你的建议卸载了一些仍在使用的事件处理程序,这显著降低了内存使用量。感谢你的帮助。 - Jacotb

5

首先,GC(垃圾回收)功能正常且运行良好。你刚刚发现的问题并不是它的错误。

既然我们已经解决了这个问题,以下是一些想法:

  1. 你是否使用了过多的线程?
  2. 请记住,GC是非确定性的;它会在认为需要运行时运行(即使你调用了GC.Collect())。
  3. 你确定所有的引用都已经超出了作用域吗?
  4. 你最初加载到内存中的是什么?大型图像?大型文本文件?

你的分析器应该告诉你哪些内容占用了大量内存。尽可能削减最大的罪魁祸首。

此外,每隔X秒调用GC.Collect()是一个糟糕的想法,并且不太可能解决你的实际问题。


  • 我正在使用多个线程,但不会说是“太多”。我认为在任何时候都不会有超过5个线程在运行。
  • 我知道GC基本上会做它想做的事情,而GC.Collect()只是向GC提出建议,告诉它可能有一些需要清理的东西。然而,这使得调用Collect()来解决内存问题更加奇怪。
  • 我的引用明显已经超出了范围,我在不再需要它们时明确将字节数组引用设置为null。
  • 使用最多内存的是包含32位RGBA纹理数据的字节数组。
- Jacotb
1
我的引用明显超出了范围,当我不再需要它们时,我会明确地将字节数组引用设置为null,除非还有其他对byte[]的引用。话虽如此,这听起来像是LOH碎片化。分析器会告诉你确切的情况。 - Ed S.

4
在.NET中分析内存问题并不是一项微不足道的任务,你肯定需要阅读几篇好的文章并尝试不同的工具来达到结果。在调查后,我找到了以下文章:http://www.alexatnet.com/content/net-memory-management-and-garbage-collector 你也可以尝试阅读Jeffrey Richter的一些文章,比如这篇:http://msdn.microsoft.com/en-us/magazine/bb985010.aspx 从我的经验来看,OutOfMemory问题有两个最常见的原因:
1.事件处理程序 - 即使没有其他对象引用它,它们可能仍然持有对象。因此,理想情况下,您需要取消订阅事件处理程序以自动销毁对象。
2.终结器线程被处于STA模式的某个其他线程阻塞。例如,当STA线程执行大量工作时,其他线程会停止,而在终结队列中的对象无法被销毁。

我并不知道关于STA线程的那些事情。换句话说,在WPF中如果UI线程非常繁忙,垃圾回收器是无法进行回收的?您有相关文章可以分享吗? - Phil
但是,如果手动调用GC.Collect可以解决问题,那么显然不是因为GC 无法收集对象或者认为它们被根引用,而是认为收集它们并不必要。 - LukeH
@LukeH:完全正确。我真的很想知道为什么它认为收集它们不是必要的,如果系统正在抛出OutOfMemoryExceptions... - Jacotb

3

编辑:添加了一个链接到大对象堆碎片化

编辑:由于看起来是在分配和丢弃纹理时出现问题,您可以使用Texture2D.SetData来重新使用大的byte[]吗?

首先,您需要确定是管理内存还是非托管内存泄漏。

  1. 使用perfmon查看进程'.net memory# Bytes in all Heaps'和Process\Private Bytes发生了什么。比较数字和内存上升。如果私有字节的增长超过堆内存的增长,则为非托管内存增长。

  2. 非托管内存增长将指向未被处置的对象(但执行其终结器时最终会收集它们)。

  3. 如果是托管内存增长,那么我们需要查看哪个代/LOH(每个堆字节的性能计数器也有)。

  4. 如果是大对象堆字节,您将要重新考虑使用和丢弃大的byte数组。也许byte数组可以被重新使用而不是被丢弃。此外,考虑分配大的byte数组,使其成为2的幂。这样,在处理后,您将留下一个大的“空洞”,可以由相同大小的另一个对象填充。

  5. 最后一个问题是固定内存,但是我对此没有任何建议,因为我从未碰过它。


添加建议解决方案:重复使用Texture2D对象。 - agent-j
我已经在其他场合使用了SetData和FromStream。虽然没有考虑过重复使用Texture2D对象,但还是谢谢你的建议。 - Jacotb

1
我还要补充一点,如果你在进行文件访问时,请确保关闭和/或处理任何读取器或写入器。你应该在打开任何文件后立即完成匹配的1-1操作。
此外,我通常使用using子句来管理资源,比如Sql连接:
using (var connection = new SqlConnection())
{
  // Do sql connection work in here.
}

你是否在任何对象上实现了IDisposable,并可能做了一些自定义的操作导致出现了问题?我建议再检查一遍你的所有IDisposable代码。


总的来说是个好建议,但是 IDisposable 和托管内存的释放无关。 - LukeH
是的,我完全同意你的观点。但我不会假设他们仅使用托管资源。事实上,当我看到XNA游戏时,我立即想到了他们可能正在使用哪些非托管资源。 - Jonathan Nixon
关闭IO流是我总是做的事情 - 但无论如何感谢你的指点。 - Jacotb

0

垃圾回收器不考虑非托管堆。如果您正在创建许多仅是C#中较大的非托管内存的包装器对象,则会消耗您的内存,但GC无法基于此做出理性决策,因为它只看到托管堆。

您最终会陷入这样一种情况:GC收集器认为您没有内存短缺,因为您的第1代堆上的大部分内容都是8字节引用,而实际上它们就像海上的冰山一样。大部分内存在下面!

您可以利用这些GC调用:

System::GC::AddMemoryPressure(sizeOfField);
System::GC::RemoveMemoryPressure(sizeOfField);

如果您提供正确的数字,这些方法可以让垃圾回收器查看非托管内存。


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