理解.NET的垃圾回收和内存溢出异常

4
我正在解决一个在我的.NET 2.0 Windows服务应用程序中出现的OutOfMemory异常。为了更好地理解问题,我首先编写了一个简单的.NET WinForm测试应用程序,通过构建一个ArrayList直到抛出OutOfMemory异常来生成一个OOM异常。异常被捕获并记录下来,我可以点击表单按钮再次运行OOME。我发现奇怪的事情是,在第四次运行之前消耗的内存大约减少了一半。每次运行时列出的结果都是一致的。眼观TaskManager也确认了这种行为。不幸的是,在尝试获取更好的统计数据时,Perfmon冻结了。有人能解释一下为什么在3次运行后内存限制会降低吗?我对GC的理解还比较浅显。你还可以看到我在几次运行后运行了GC.Collect(),但它没有帮助降低限制。
更新:我还发现使用const字符串与为每个arraylist项创建一个新对象之间存在很大的差异。代码非常简单:
const string TEST_TEXT = "xxxxxxxxxx";
ArrayList list = new ArrayList();
while (true)
{
    list.Add(TEST_TEXT);
}

开始循环:内存 10,350,592

  • 抛出OOM异常
  • 数组大小:134,217,728

结束循环:内存550,408,192

开始循环:内存 550,731,776

  • 抛出OOM异常
  • 数组大小:134,217,728

结束循环:内存 551,682,048

开始循环:内存 551,813,120

  • 抛出OOM异常
  • 数组大小:134,217,728

结束循环:内存 551,772,160

开始循环:内存 551,903,232

  • 抛出OOM异常
  • 数组大小:67,108,864

结束循环:内存282,869,760

开始循环:内存 283,004,928

  • 抛出OOM异常
  • 数组大小:67,108,864

结束循环:内存 282,910,720

手动触发GC.Collect

开始循环:内存14,245,888

  • 抛出OOM异常
  • 数组大小:67,108,864

结束循环:内存283,344,896


你的测试程序具体是做什么的?它如何“构建一个数组”? - jalf
请发布您的代码。 - Michael Petrotta
请参见上面的代码示例。 - HH.
类似这样的字符串字面量是国际化的。每次添加到您的 ArrayList 中,只会创建对同一块内存的新引用(此外,在 .Net 2.0 及更高版本中,ArrayList 是邪恶的:不要使用它)。 - Joel Coehoorn
1
只是好奇为什么在2.0+中ArrayList被认为是邪恶的,相比使用通用等效物? - HH.
2个回答

8
以下是几点内容,希望综合考虑后能为您解答问题:
  • 尽管名称如此,内存不足异常同样可能意味着你的地址空间已满,而非实际物理内存。
  • GC.Collect并未收集所有未使用内存。.Net中垃圾回收是非确定性的,这意味着无法强制运行时清除所有内存。
  • .Net中的垃圾收集器是分代的,这意味着当一个对象经过收集后,它会被移到更高一代,使得它更不可能被收集。
  • 在抛出OutOfMemory异常时,您的数组可能已经经历了几次收集尝试,甚至已经移动到了大对象堆。
  • 数组大小是固定的。要向数组添加新元素,必须完全重新分配数组。(使用类似列表的结构可能会获得更好的测试结果)。

3

由于您正在构建数组,我假设您正在为每个运行构建一个大数组。如果是这种情况,它将存储在大对象堆(因为它将大于85000字节)。 LOH不像分代堆一样被压缩,所以您看到的大小降低可能是由于堆碎片化引起的。


现在很少有理由使用ArrayList,但在这种情况下并没有什么区别,因为ArrayList也在内部使用数组。 - Brian Rasmussen

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