了解内存性能计数器

6
[更新-2010年9月30日]
由于我在这方面学习了很多相关的知识,我将写下我根据自己的经验和答案中提供的建议所总结出来的技巧-
1) 使用内存分析器(从CLR Profiler开始尝试),找出消耗最大内存的例程并进行微调,如重复使用大型数组,尽量减少对象的引用。
2) 如果可能,分配小对象(对于.NET 2.0而言,小于85k),如果可以使用内存池,则避免垃圾回收器产生高CPU使用率。
3) 如果增加对象的引用,则需要相同次数地取消引用它们。这样做会让你放心,代码也可能更好地工作。
4) 如果什么都不管用,你仍然毫无头绪,那么请使用排除法(注释/跳过代码)找出消耗最多内存的是什么。
在代码内部使用内存性能计数器也可能有所帮助。
希望这些能够帮到你!
[原始问题]
嗨!
我正在使用C#编程,我的问题是内存不足异常。
我在这里读了一篇关于LOH的优秀文章 -> http://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/ 太棒了!
还有这篇文章: http://dotnetdebug.net/2005/06/30/perfmon-your-debugging-buddy/ 我的问题: 我正在面对企业级桌面应用程序的内存不足问题。我尝试阅读和理解有关内存分析和性能计数器的内容(也尝试过WinDBG!-稍微了解了一点),但仍然对基本知识毫无头绪。
我尝试使用CLR分析器来分析内存使用情况。它在以下方面很有帮助:
1) 显示哪些代码分配了大块内存
2) 哪种数据类型使用了最多的内存
但是,CLR Profiler和性能计数器(因为它们共享相同的数据)都无法解释:
1) 在每次运行应用程序后收集的数字 - 如何确定是否有任何改进???
2) 如何比较每次运行后的性能数据 - 特定计数器的较低/较高数字是好还是坏?
我需要的: 我正在寻找以下几个方面的技巧:
  1. 如何释放(是的,没错)托管数据类型对象(例如数组、大字符串),但不通过调用GC.Collect来实现,如果可能的话。我不得不处理每隔一段时间长度为500KB的字节数组(无法避免大小:-()。

  2. 如果发生碎片,如何压缩内存——因为似乎.NET GC并没有真正有效地做到这一点,导致OOM。

  3. 此外,LOH的85KB限制是指对象的大小还是数组的总大小?对我来说不是很清楚。

  4. 哪些内存计数器可以告诉我们代码更改是否真正减少了OOM的机会?

我已经知道的提示

  1. 将托管对象设置为null——标记它们为垃圾——以便垃圾收集器可以收集它们。这很奇怪——在将string[]对象设置为null后,所有堆中的字节数飙升了!

  2. 避免创建大于85KB的对象/数组——这不在我的控制范围内。因此,可能会有很多LOH。

3.

内存泄漏指标:
所有堆中的字节数增加 第2代堆大小增加 GC句柄数增加 固定对象数增加 总提交字节数增加 总保留字节数增加 大对象堆增加

我的情况:

  • 我有一台4GB、32位机器,上面安装了Wink 2K3服务器SP2。
  • 我知道一个应用程序可以使用<=2GB的物理RAM。
  • 在这种情况下,增加虚拟内存(页面文件)大小没有效果。

由于是OOM问题,我只关注与内存相关的计数器。

请给予建议!我真的需要一些帮助,因为缺乏好的文档而陷入困境!


我明白,Henk。但是我不能提供代码。此外,我只是在寻找“指导方针”,以减轻内存负担。如果你能帮忙,谢谢! :) - Nayan
3个回答

2
您可以尝试自己池化和管理大型对象。例如,如果您经常需要<500k的数组,并且同时存活的数组数量是已知的,那么您可以避免永远释放它们-这样,如果您一次只需要10个,您可以遭受固定的5mb内存开销,而不是麻烦的长期碎片化。
至于您的三个问题:
1. 这是不可能的。只有垃圾回收器决定何时完成托管对象并释放其内存。这是托管对象的一部分。 2. 如果您在不安全的代码中管理自己的堆栈并完全绕过大型对象堆,则可以实现此目标。如果您走这条路,最终会做很多工作并遭受很多不便。我怀疑这对您来说是否值得。 3. 它是对象的大小,而不是数组中的元素数量。
请记住,碎片化只发生在对象被释放时,而不是在它们被分配时。如果碎片化确实是您的问题,请重用大型对象。专注于在应用程序的生命周期内创建更少的垃圾(特别是大型垃圾),而不是直接处理gc实现的细节。

好主意,但请理解这是企业级应用程序。请理解应用程序的规模和大小。因此,我不能进行如此巨大的设计更改。有没有其他关于优化的想法? :) - Nayan
除此之外,如果不使用非托管代码,如何在托管代码中使用内存池呢?有没有好的资料可以参考? - Nayan
1
“Magnitude and size” 当然是问题的根源。这里唯一可用的魔法就是一个64位操作系统。只需花费200美元即可解决您的问题。 - Hans Passant
笑..好建议汉斯! :) 但我怀疑我的公司会同意。这是使用如此广泛且设计不佳的企业产品的常见问题 =D - Nayan
当我建议您池化和管理对象时,我是在建议您重复使用受管理的对象...您不需要涉及非托管代码来挂起数组列表并重复使用它们。 - blucz
好的Blucz,你有没有实现这些东西的好例子?比如,托管内存池?有没有好的文档可以参考?谢谢! - Nayan

2

Nayan,以下是你的问题的答案以及一些额外的建议。

  1. 你不能释放它们,只能使它们更容易被GC收集。看起来你已经知道方法了:关键是减少对对象的引用数量。
  2. 碎片化是另一件你无法控制的事情。但有几个因素可以影响它:
    • LOH外部碎片不像Gen2外部碎片那么危险,因为LOH没有压缩。LOH的空闲插槽可以重复使用。
    • 如果500Kb字节数组用作某些IO缓冲区(例如传递给某些基于套接字的API或非托管代码),则很可能会被固定。固定的对象无法被GC压缩,它们是堆碎片化最常见的原因之一。
    • 85K是对象大小的限制。但请记住,System.Array实例也是一个对象,因此所有500K字节[]都在LOH中。
    • 您帖子中的所有计数器都可以提示内存消耗的变化,但在您的情况下,我会选择BIAH(所有堆中的字节数)和LOH大小作为主要指标。BIAH显示所有托管堆(Gen1 + Gen2 + LOH,确切地说,没有Gen0 - 但谁关心Gen0呢?:))的总大小,LOH是放置所有大字节[]的堆。

建议:

  • 已经提出的建议:预分配和缓存您的缓冲区。

  • 另一种方法是,如果您可以使用任何集合而不是连续的字节数组(如果缓冲区用于IO,则不是这种情况):实现一个自定义集合,其内部将由许多较小大小的数组组成。这类似于C++ STL库中的std::deque。由于每个单独的数组都比85K小,整个集合就不会进入LOH。您可以通过此方法获得以下优势:仅在进行完整GC时才会收集LOH。如果您的应用程序中的byte[]不是长期存在的,并且(如果它们的大小更小)在被收集之前将进入Gen0或Gen1,那么这将使GC的内存管理变得更加容易,因为Gen2收集要重量级得多。

  • 有关测试和监视方法的建议:根据我的经验,需要长时间监视GC行为、内存占用和其他与内存相关的内容,以获得一些有效和稳定的数据。因此,每次更改代码时,请进行足够长的测试,并监视内存性能计数器以查看更改的影响。

  • 我还建议查看%Time in GC计数器,因为它可以是内存管理有效性的良好指标。这个值越大,你的应用程序花费在GC例程上的时间就越多,而不是处理用户请求或执行其他“有用”的操作。我无法为此计数器的绝对值提供建议,以指示问题,但我可以分享我的经验供您参考:对于我正在工作的应用程序,我们通常将%Time in GC高于20%视为问题。

此外,如果您共享应用程序的与内存相关的性能计数器的一些值,则会很有用:进程的私有字节和工作集、BIAH、总提交字节、LOH大小、Gen0、Gen1、Gen2大小、Gen0、Gen1、Gen2收集次数、%Time in GC。这将有助于更好地了解您的问题。


Alexey,你真的帮了我很多!这是一个罕见的时刻,有人诚实地回答并尝试帮助,而不是不必要地讲道理。非常感谢你的建议。我会回来分享一些反向统计数据,以便更准确地描述问题的本质!祝好! - Nayan
请澄清一下...你说的“LOH的空闲槽位可以被GC或程序员重复利用”,是由垃圾回收器还是由程序员来完成? - Nayan
你能回答一下这个问题吗(这让我感到困惑)- 每次测试运行后更高的 BIAH 数字是好还是坏? - Nayan
是垃圾收集器可以重用 LOH 内的空闲空间来分配新对象。更多关于 LOH 的阅读:http://msdn.microsoft.com/en-us/magazine/cc534993.aspx - Alexey Nedilko
对于 BIAH 的大小,是否更小更好并没有通用的答案(每个应用程序都有自己的内存使用模式),但在 OOM 条件下,较小的堆(因此较小的 BIAH)应该是您的目标。 - Alexey Nedilko

1
另一个指标是观察“Private Bytes”与“Bytes in all Heaps”的比较。如果“Private Bytes”增长速度比“Bytes in all Heaps”快,那么就存在未管理的内存泄漏。如果“Bytes in all Heaps”增长速度比“Private Bytes”快,则是托管泄漏。
要更正@Alexey Nedilko所说的内容:绝对是错误的。Gen2是紧凑的,这意味着在收集后永远不会有空闲空间。LOH没有压缩(正如他正确地提到的),是的,可以重用空闲插槽。但是,如果剩余空间不连续以适应请求的分配,则段大小会增加 - 并且可能会继续增长。因此,您可能会遇到永远不会填充的LOH中的间隙。这是OOM的常见原因,我在分析的许多内存转储中都看到过这种情况。
虽然现在GC API中有方法(自.NET 4.51起)可以调用以编程方式压缩LOH,但我强烈建议避免这样做——如果应用程序的性能是一个问题。在运行时执行此操作非常昂贵,并且会显著损害您的应用程序性能。默认实现的GC之所以要高效,是因为他们在第一次省略了这一步。在我看来,如果您发现必须调用此函数,原因是您的应用程序出现了LOH碎片,那么您的应用程序中肯定存在问题,可以通过池技术、拆分数组和其他内存分配技巧进行改进。如果这个应用程序是离线应用程序或某些批处理过程,性能不是很重要,也许情况并不那么糟糕,但最好只是适度使用。

这里有一个很好的视觉例子,说明了这种情况如何发生 - 大对象堆的危险,以及这里大对象堆揭秘 - Maoni(CLR上的GC团队领导)


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