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()
时,内存使用问题似乎已经解决了。有人知道是什么原因导致内存使用率如此之高吗?它可能与在调试模式下运行有关吗?
搜索泄漏失败后
更加努力=)
托管语言中的内存泄漏可能很难追踪。我对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工作正常,工作良好。这里面没有你刚刚发现的漏洞。
现在我们已经解决了这个问题,一些想法:
- 你用的线程太多了吗
- 记住GC是不确定的;它会在它认为需要运行的时候运行(即使您调用
GC.Collect()
- 你确定你所有的推荐信都超出了范围吗
- 你一开始在记忆中加载了什么?大图像?大型文本文件
你的探查器应该告诉你是什么用了这么多内存。开始尽可能多地打击罪魁祸首。
另外,每隔X秒调用GC.Collect()是个坏主意,不太可能解决实际问题。
分析中的内存问题。NET不是一项琐碎的任务,你肯定应该阅读几篇好文章,并尝试不同的工具来获得结果。经过调查,我最终发表了以下文章:http://www.alexatnet.com/content/net-memory-management-and-garbage-collector你也可以试着阅读杰弗里·里希特的一些文章,比如这篇:http://msdn.microsoft.com/en-us/magazine/bb985010.aspx
根据我的经验,内存不足问题有两个最常见的原因:
- 事件处理程序-即使没有其他对象引用对象,它们也可能保存对象。因此,理想情况下,您需要取消订阅事件处理程序才能自动销毁对象
- 终结器线程在STA模式下被其他线程阻塞。例如,当STA线程执行大量工作时,其他线程将停止,并且最终队列中的对象无法销毁
编辑:添加了到大型对象堆碎片的链接。
编辑:由于分配和丢弃纹理看起来有问题,您可以使用Texture2D吗。SetData是否重用大字节[]?
首先,您需要弄清楚泄漏的是托管内存还是非托管内存。
-
使用perfmon查看进程".net内存#所有堆中的字节数"和
Process'Private Bytes
发生了什么。比较这些数字,记忆就会上升。如果Private字节的增长速度超过了堆内存的增长速度,那么这就是非托管内存的增长。 -
非托管内存增长将指向未被释放的对象(但最终在其终结器执行时被收集)。
-
如果是托管内存增长,那么我们需要了解哪一代/LOH(每一代堆字节也有性能计数器)。
-
如果是大型对象堆字节,则需要重新考虑使用和丢弃大型字节数组。也许字节数组可以重复使用而不是丢弃。另外,考虑分配2的幂的大字节数组。这样,当被处理时,您将在大型对象堆中留下一个大的"洞",可以由另一个相同大小的对象填充。
-
最后一个问题是固定记忆,但我没有任何建议给你,因为我从来没有搞砸过它
我还想补充一点,如果您正在进行任何文件访问,请确保关闭和/或处理任何读卡器或写入器。打开任何文件和关闭文件之间应该有一个匹配的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);
这些方法允许垃圾收集器查看非托管内存(如果您提供的是右图)