C#.NET垃圾回收未运行

本文关键字:运行 NET | 更新日期: 2023-09-27 17:59:23

我正在Visual Studio 2010中开发一个相对较大的解决方案。它有各种各样的项目,其中一个是XNA游戏项目,另一个是ASP.NET MVC 2项目。

对于这两个项目,我都面临着相同的问题:在调试模式下启动它们后,内存使用率不断上升。它们分别从40和100MB的内存使用量开始,但都相对较快地攀升到1.5GB(分别为10和30分钟)。在那之后,它有时会回落到接近初始使用,而其他时候它只会抛出OutOfMemoryExceptions

当然,这会表明内存严重泄漏,所以这就是我最初试图发现问题的地方。在搜索泄漏失败后,我尝试定期调用GC.Collect()(大约每10秒调用一次)。在引入这个"破解"后,内存使用量分别保持在45和120MB达24小时(直到我停止测试)。

.NET的垃圾收集被认为是"非常好"的,但我忍不住怀疑它只是不起作用。我曾使用CLR Profiler尝试解决该问题,结果表明XNA项目似乎保存了我确实在使用的许多字节数组,但对这些数组的引用应该已经删除,因此由垃圾收集器收集。

同样,当我定期调用GC.Collect()时,内存使用问题似乎已经解决了。有人知道是什么原因导致内存使用率如此之高吗?它可能与在调试模式下运行有关吗?

C#.NET垃圾回收未运行

搜索泄漏失败后

更加努力=)

托管语言中的内存泄漏可能很难追踪。我对Redgate ANTS内存档案器有很好的体验。这不是免费的,但他们会给你14天的全功能试用。它有一个很好的UI,可以向您显示内存的分配位置以及为什么这些对象被保存在内存中。

正如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[]。将流封装在伪流中并使其无效解决了这个问题。

此外,声明显而易见的是,确保实现IDisposable的对象的Dispose()。周围可能有本地资源。同样,一个好的探查器会捕捉到这一点。

我的建议;不是GC,问题出在你的应用程序上。使用探查器,使您的应用程序处于高内存消耗状态,获取内存快照并开始分析。

首先,GC工作正常,工作良好。这里面没有你刚刚发现的漏洞。

现在我们已经解决了这个问题,一些想法:

  1. 你用的线程太多了吗
  2. 记住GC是不确定的;它会在它认为需要运行的时候运行(即使您调用GC.Collect()
  3. 你确定你所有的推荐信都超出了范围吗
  4. 你一开始在记忆中加载了什么?大图像?大型文本文件

你的探查器应该告诉你是什么用了这么多内存。开始尽可能多地打击罪魁祸首。

另外,每隔X秒调用GC.Collect()是个坏主意,不太可能解决实际问题。

分析中的内存问题。NET不是一项琐碎的任务,你肯定应该阅读几篇好文章,并尝试不同的工具来获得结果。经过调查,我最终发表了以下文章:http://www.alexatnet.com/content/net-memory-management-and-garbage-collector你也可以试着阅读杰弗里·里希特的一些文章,比如这篇:http://msdn.microsoft.com/en-us/magazine/bb985010.aspx

根据我的经验,内存不足问题有两个最常见的原因:

  1. 事件处理程序-即使没有其他对象引用对象,它们也可能保存对象。因此,理想情况下,您需要取消订阅事件处理程序才能自动销毁对象
  2. 终结器线程在STA模式下被其他线程阻塞。例如,当STA线程执行大量工作时,其他线程将停止,并且最终队列中的对象无法销毁

编辑:添加了到大型对象堆碎片的链接。

编辑:由于分配和丢弃纹理看起来有问题,您可以使用Texture2D吗。SetData是否重用大字节[]?

首先,您需要弄清楚泄漏的是托管内存还是非托管内存。

  1. 使用perfmon查看进程".net内存#所有堆中的字节数"和Process'Private Bytes发生了什么。比较这些数字,记忆就会上升。如果Private字节的增长速度超过了堆内存的增长速度,那么这就是非托管内存的增长。

  2. 非托管内存增长将指向未被释放的对象(但最终在其终结器执行时被收集)。

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

  4. 如果是大型对象堆字节,则需要重新考虑使用和丢弃大型字节数组。也许字节数组可以重复使用而不是丢弃。另外,考虑分配2的幂的大字节数组。这样,当被处理时,您将在大型对象堆中留下一个大的"洞",可以由另一个相同大小的对象填充。

  5. 最后一个问题是固定记忆,但我没有任何建议给你,因为我从来没有搞砸过它

我还想补充一点,如果您正在进行任何文件访问,请确保关闭和/或处理任何读卡器或写入器。打开任何文件和关闭文件之间应该有一个匹配的1-1

此外,我通常对资源使用using子句,比如SqlConnection:

using (var connection = new SqlConnection())
{
  // Do sql connection work in here.
}

您是否在任何对象上实现了IDisposable,并可能执行了导致任何问题的自定义操作?我会仔细检查你所有的IDisposable代码。

GC不考虑非托管堆。如果你在C#中创建了很多对象,这些对象只是对更大的非托管内存的包装,那么你的内存就会被吞噬,但GC无法基于此做出合理的决定,因为它只看到托管堆。

您最终会遇到GC收集器并不认为您内存不足的情况,因为第1代堆上的大多数内容都是8字节的引用,而实际上它们就像海上的冰山。大部分记忆都在下面!

您可以使用以下GC调用:

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

这些方法允许垃圾收集器查看非托管内存(如果您提供的是右图)