.NET的内存使用情况(如何防止过度分配/释放内存给操作系统)

17

我目前正在开发一个网站,该网站大量使用缓存数据以避免往返传输。

在启动时,我们会获取一个“大型”图形(数十万个不同种类的对象)。

这些对象通过WCF进行检索和反序列化(我们使用协议缓冲区进行序列化)。

我正在使用redgate的内存分析器来调试内存问题(内存似乎与我们初始化后应需要的内存量不符,并且最终得到以下报告:

全局报告

现在我们可以从这份报告中得出以下结论:

1).NET分配的大部分内存是空闲的(它可能在反序列化期间被正确地分配,但现在已经空闲,希望它返回到操作系统中)。

2)内存是碎片化的(这很糟糕,因为每次刷新缓存时,我都需要重新进行耗费内存的反序列化过程,这反过来会创建大型对象,由于碎片化而可能引发OutOfMemoryException)。

3)我不知道空间为什么是碎片化的,因为当我查看大型对象堆时,只有30个实例,其中15个object[]直接附加到GC并与我无关,1个char数组也直接附加到GC堆上,其余的15个都是我的,但不是造成这个问题的原因,如果我在代码中注释掉它们,我会得到相同的报告。

所以我的问题是,我该怎么做才能进一步解决这个问题? 我不太确定在调试/工具中要寻找什么,因为似乎我的内存是碎片化的,但不是由我造成的,并且大量的空闲空间由.NET分配,我无法释放。

此外,请确保在回答前充分理解问题,我不是在寻找释放.NET内存(GC.Collect)的方法,而是释放已经在.NET中空闲的内存,并将该内存碎片整理好后返回给操作系统。

请注意,如果可以手动碎片整理大堆内存,即使解决方案较慢,也没有关系,因为我可以在RefreshCache的末尾调用它,并且执行需要1或2秒也无妨。
感谢您的帮助!
我忘记了几点: 1)该项目是一个.NET 2.0网站,在.NET 4池中运行它会得到相同的结果,即使我将其转换为.NET 4并重新编译也是如此。
2)这些都是发布版本的结果,因此调试版本不可能是问题所在。
3)这可能非常重要,我在WebDev服务器中根本没有遇到这些问题,只有在IIS中才有,而在WebDev中,我得到的内存消耗非常接近我的实际消耗(多一些,但不是5-10倍!)

你的服务器应用程序池和开发服务器之间的处理器架构有区别吗? - Rowland Shaw
很抱歉,我的句子表达不够清晰。我并不是指开发服务器作为一个独立的服务器,而是在同一台服务器上运行但在IIS之外(使用Visual Studio 2010集成的ASP.NET开发服务器)。 - Ronan Thibaudau
在同一主题下,那么在IIS Express下呢?我期望与IIS相同的行为。不确定是否有用。 - Peter T. LaComb Jr.
我不知道,但这不会改变太多,无论如何我需要它在真正的IIS下运行。我只是希望指出asp.net开发服务器不显示这个问题可能有助于调试。 (请注意,如果可以帮助的话,我很乐意在iis express下进行测试,但我不确定是否应该在已经安装了iis的服务器上安装它) - Ronan Thibaudau
内存碎片化是由于本地对象引起的吗?您是否有未托管的对象? - Seb
显示剩余3条评论
6个回答

9

在大对象堆上分配的对象(>= 85,000字节,通常是数组)不会被垃圾回收器压缩。微软认为移动这些对象的成本太高。

建议尽可能重用大对象,以避免托管堆和VM空间的碎片化。

http://msdn.microsoft.com/en-us/magazine/cc534993.aspx

我假设您的大对象是由反序列化库创建的临时字节数组。如果该库允许您提供自己的字节数组,您可以在程序开始时预先分配它们,然后重复使用它们。

8
我知道这不是你想听到的答案,但是你不能强制地将内存释放回操作系统。不过,你为什么要这样做呢?当你的物理内存不足时,.NET会将其堆释放回操作系统。但是,如果有充足的空闲物理内存,.NET将保留其堆以使未来分配对象更快。如果你真的想强制.NET将其堆释放回操作系统,我想你可以编写一个C程序,只需malloc直到内存用尽。这应该会导致操作系统向.NET发出信号,以释放其未使用的堆。

最好将未使用的内存保留给.NET,以便您的应用程序将具有更好的分配性能(因为运行时知道哪些内存是空闲的,哪些不是,分配可以直接使用空闲内存而不必在操作系统中调用系统调用以获取更多内存)。
垃圾收集器负责碎片整理堆。定期(通常在收集期间),如果决定需要这样做,它会移动堆中的对象。(这就是为什么C++/CLI有pin_ptr构造“固定”对象的原因)。
不过,内存碎片通常不是一个大问题,因为它提供快速的随机访问。
至于你的OutOfMemoryException,我没有一个好的答案。通常我会怀疑你的旧对象图没有被收集(某个对象在某处持有对其的引用,即“内存泄漏”)。但既然你正在使用分析器,那我就不知道了。

你的代码全部都是托管C#,对吧?你没有将指针固定以便于在非托管代码中进行封送吗?否则,运行时应该会为您整理内存碎片。 - atanamir
不,全部是托管代码,而且我的应用程序不需要更好的分配性能,它只需要每天分配一次,并且是非阻塞操作。这里的问题是它过度分配了多少(5倍于所需),以及内存碎片化。据我所知,GC不会对大对象堆进行碎片整理,这就是导致OutOfMemoryExceptions的原因,我看到有帖子抱怨没有办法触发大对象堆碎片整理。问题在于,每天刷新所有内容,由于碎片化而每隔几天就出现OutOfMemoryException是绝对不能接受的。 - Ronan Thibaudau
您可以尝试使用一个简单的C程序,其中包含一个带有malloc()的循环,一旦malloc()失败(返回值为NULL),就会退出程序。然后退出程序。这应该强制.NET释放其所有内存(但也会使运行在操作系统上的所有进程执行相同的操作)。 - atanamir
但这并不能解决碎片化的问题,据我所知,垃圾收集器不会释放碎片化的部分,只会释放完全空闲的段,所以虽然它可以释放一些过度分配的内存,但我仍然会受到自由碎片化内存的束缚,这可能导致OutOfMemoryExceptions的发生。我真希望有一个GC.CompactAllGen()方法。 - Ronan Thibaudau
如果它们不是大对象,.NET运行时应该通过将所有内容移动到堆的开头来重新排列堆,然后释放已释放的内存部分。您确定您的分析器显示了“持续”的碎片问题,还是仅在快照时间显示了碎片(例如,在运行时的碎片整理之间)? - atanamir
分析器没有给我细节(连续查看内存会很好),但它确实说到内存碎片化限制了未来分配的大小,这只在大对象堆的情况下才有意义;此外,它报告了 15 个对象的 15 个片段,这不是一个好现象,但这些对象都不是由我管理的(它们是 GCRoot 对象)。 - Ronan Thibaudau

4
从.NET 4.5.1开始,您可以设置一次性标志,在调用GC collect之前压缩LOH,即:
Runtime.GCSettings.LargeObjectHeapCompactionMode = System.Runtime.GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect(); //这将导致LOH被压缩(一次)。

2
一些测试和一些C ++后,我找到了为什么会有这么多的空闲内存的原因。这是因为IIS通过VM Hoarding实例化CLR(提供一个dll来实例化它而不需要VM Hoarding占用相同数量的初始内存,但随着时间的推移会释放大部分内存,这是我期望的行为)。所以这个确实解决了我报告的内存问题,然而无论如何,我仍然得到约100MB的空闲内存,并且我仍然认为这是由于碎片化造成的,只有在一次性释放所有碎片时才会被释放,因为分析器仍然报告内存碎片化。所以我没有将自己的答案标记为答案,希望有人能为此提供一些指导或直接引导我去使用工具来修复或帮助我调试根本原因。

我因为完全相同的原因遇到了完全相同的问题。我也追踪到它是由于虚拟机垃圾回收导致的,尽管使用WinDbg + SOS + SOSEX来检查进程转储和“高级.NET调试”作为参考(它实际上提到了垃圾回收作为新增的.NET 2功能)。目前我还卡在这里。 - bushed
我正在考虑的潜在解决方案之一可能是为预缓存来自数据库的数据启动单独的进程(我们正在使用分布式缓存)。 - bushed
嗨Ronan,我发现我的应用程序似乎遇到了完全相同的问题。你说VM Hoarding是问题所在;你是如何关闭它以适用于IIS的?或者你是通过其他方式绕过它的? - Dave
Ronan,我和Dave有同样的问题。你是如何关闭IIS的?或者这是否可能? - Uchitha

1

有趣的是,它在WebDevServer和IIS上的工作方式不同...

IIS是否使用服务器垃圾回收器,而WebDev服务器使用工作站垃圾回收器?垃圾回收的方法可能会影响碎片化。这可能已经在您的aspnet.config文件中设置了。请参见:http://support.microsoft.com/kb/911716


-1
如果你还没有找到答案,我认为以下的线索可以帮助你:

回到基础知识:有时我们会忘记对象可以显式地释放,明确调用对象的Dispose方法(因为你没有提到它,我假设你使用的是"object = null"指令)。

使用继承的方法,除非你的类没有该方法,否则你不需要实现它,我对此表示怀疑。

MSDN帮助关于这个方法的说明如下:

... 在只使用托管资源(例如数组)的类型上实现Dispose方法没有性能优势,因为它们将被垃圾收集器自动回收。主要在使用本机资源的托管对象和暴露给.NET Framework的COM对象上使用Dispose方法。 ...

因为它说"它们将被垃圾收集器自动回收",我们可以推断出当调用该方法时会进行"释放操作"(再次强调,我只是试图给你一些提示)。

此外,我发现了这篇有趣的文章(我猜...我没有完全阅读它):Garbage Collection: Automatic Memory Management in the Microsoft .NET Framework (http://msdn.microsoft.com/en-us/magazine/bb985010.aspx)。在“强制清理对象”部分中,它指出:

...建议您为类型添加一个额外的方法,允许类型的用户在需要时显式地清理对象。按照惯例,此方法应称为Close或Dispose....

如果您仔细阅读这篇文章或继续朝这个方向进行调查,也许答案就在其中。


2
当垃圾回收器释放一个对象(包括Dispose()或其他方式)时,被回收的内存会进入进程的“空闲空间”堆中。这就是“蓝色”饼图片段。问题在于如何将该内存释放回操作系统,以便其他进程可以使用它。 - crokusek

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