这是一个 OutOfMemoryException,所以这不是这里所讨论的集合的大小或容量:它是您的应用程序中的内存使用。诀窍是您不必用尽机器甚至进程中的内存来获取此异常。
我认为正在发生的事情是您正在填满大型对象堆。随着收藏的增长,他们需要在后台添加存储以容纳新项目。一旦分配了新的存储并复制了项目,旧的存储就会被释放并且应该可以进行垃圾回收。
问题在于,一旦超出特定大小(过去是 85000 字节,但现在可能不同),垃圾收集器 (GC) 就会使用称为大对象堆 (LOH) 的东西来跟踪您的内存。当 GC 从 LOH 中释放内存时(这种情况开始时很少发生),内存将返回到您的操作系统并可供其他进程使用,但该内存中的 虚拟地址空间仍将在您自己的流程中使用。您的程序地址表中会出现一个巨大的漏洞,并且因为这个漏洞位于大对象堆上,所以它永远不会被压缩或回收。
您在 2 的精确幂次方上看到此异常的原因是大多数 .Net 集合使用加倍算法来向集合添加存储。它总是会在需要再次加倍的地方抛出,因为在那之前已经分配了 RAM。
因此,一个快速的解决方案是利用大多数 .Net 集合中很少使用的功能。如果您查看构造函数重载,大多数集合类型都会有一个允许您在初始构造期间设置容量。此容量不是硬性限制——它只是一个起点——但它在少数情况下很有用,包括当你的集合会变得非常大时。您可以将初始容量设置为淫秽的东西……希望足够大以容纳所有物品,或者至少只需要“加倍”一次或两次。
您可以通过在控制台应用程序中运行以下代码来看到这种效果:
var x = new List<int>();
for (long y = 0; y < long.MaxValue; y++)
x.Add(0);
在我的系统上,在 134217728 个项目之后会引发 OutOfMemory 异常。 134217728 * 每个 int 4 个字节仅(并且确切地说)512MB 的 RAM。它不应该抛出,因为这是该过程中唯一具有任何实际大小的东西,但无论如何它都会因为地址空间丢失给旧版本的集合。
现在让我们更改代码来设置容量,如下所示:
var x = new List<int>(134217728 * 2);
for (long y = 0; y < long.MaxValue; y++)
x.Add(0);
现在我的系统在抛出时会一直运行到 268435456 个项目(1GB 的 RAM),因为它不能将 1GB 翻倍,这要归功于进程使用的其他 ram 吃掉了 2GB 虚拟地址表的一部分限制(即:循环计数器以及集合对象和进程本身的任何开销)。
我无法解释的是,它不允许我使用 3 作为乘数,即使那只是(!)1.5GB。一个使用不同乘数的小实验试图找出我能得到多大,结果表明这个数字并不一致。在某一时刻,我能够达到 2.6 以上,但后来不得不回落到 2.4 以下。我猜是要发现一些新东西。
如果此解决方案确实为您提供了足够的空间,那么还有一个trick you can use to get 3GB of virtual address space,或者您可以强制您的应用程序编译为 x64 而不是 x86 或 AnyCPU。如果您使用的是基于 2.0 运行时的框架版本(通过 .Net 3.5 的任何版本),您可能会尝试更新到 .Net 4.0 或更高版本,据报道这会更好一些。如果做不到这一点,您将不得不完全重写您如何处理数据,这可能涉及将其保存在磁盘上,并且一次只在内存中保存一个项目或项目的小样本(缓存)。我真的推荐最后一个选项,因为其他任何东西最终都可能会再次意外中断(如果你的数据集一开始就这么大,它也可能会增长)。