这是一个 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),之所以这样做,是因为由于进程使用的其他 RAM 占用了 2GB 虚拟地址表限制的一部分(即,它无法将 1GB 加倍) :循环计数器以及来自集合对象和进程本身的任何开销)。
我无法解释的是,它不允许我使用 3 作为乘数,即使那只是(!)1.5GB。使用不同乘数的一个小实验试图找出我能得到多大的结果,结果表明这个数字并不一致。有一次我能够达到 2.6 以上,但后来不得不回退到 2.4 以下。我想有新的发现。
如果这个解决方案确实为您提供了足够的空间,那么还有一个可以用来获得 3GB 虚拟地址空间的技巧 https://stackoverflow.com/questions/464458/how-do-i-create-a-32-bit-net-application-to-use-3-gb-ram,或者您可以强制您的应用程序针对 x64 而不是 x86 或 AnyCPU 进行编译。如果您使用的是基于 2.0 运行时的框架版本(从 .Net 3.5 开始的任何版本),您可以尝试更新到 .Net 4.0 或更高版本,据报道这会更好一些。如果做不到这些,您将不得不彻底重写如何处理数据,这可能涉及将数据保存在磁盘上,并且一次仅在内存中保存单个项目或项目的小样本(缓存)。我真的推荐最后一个选项,因为其他任何东西最终都可能会意外地再次中断(如果你的数据集一开始就这么大,它也可能会增长)。