当仍有大量可用内存时,抛出“System.OutOfMemoryException”异常。

120

以下是我的代码:

int size = 100000000;
double sizeInMegabytes = (size * 8.0) / 1024.0 / 1024.0; //762 mb
double[] randomNumbers = new double[size];

异常: 引发了类型为“System.OutOfMemoryException”的异常。

我的机器有4GB内存,其中2.5GB可用,当我开始运行时,显然PC上有足够的空间来处理1亿个随机数中的762MB。我需要尽可能多地存储随机数,以便利用可用内存。在生产环境中,该计算机将拥有12GB内存,我希望能够利用它。

CLR是否限制了我默认的最大内存?如何请求更多内存?

更新

如果问题是由于内存碎片化引起的,我认为将其分成较小的块并逐步增加内存需求会有所帮助,但事实并非如此。无论我如何调整块大小,我都无法超过总ArrayList大小256MB

private static IRandomGenerator rnd = new MersenneTwister();
private static IDistribution dist = new DiscreteNormalDistribution(1048576);
private static List<double> ndRandomNumbers = new List<double>();

private static void AddNDRandomNumbers(int numberOfRandomNumbers) {
    for (int i = 0; i < numberOfRandomNumbers; i++) {
      ndRandomNumbers.Add(dist.ICDF(rnd.nextUniform()));                
  }
}

从我的主方法:

int blockSize = 1000000;

while (true) {
  try
  {
    AddNDRandomNumbers(blockSize);                    
  }
  catch (System.OutOfMemoryException ex)
  {
    break;
  }
}            
double arrayTotalSizeInMegabytes = (ndRandomNumbers.Count * 8.0) / 1024.0 / 1024.0;

7
我建议重新设计你的应用程序,这样你就不必使用那么多的内存。你在做什么事情,需要一次性在内存中保存一亿个数字吗? - Eric Lippert
2
你没有禁用页面文件或者做了什么傻事吧? - jalf
@EricLippert,我在解决P vs. NP问题(http://www.claymath.org/millenium-problems/p-vs-np-problem)时遇到了这个问题。您有减少工作内存使用的建议吗?(例如,将数据块序列化并存储在硬盘上,使用C++数据类型等) - devinbost
@bosit 这是一个问答网站。如果您有关于实际代码的具体技术问题,请将其发布为问题。 - Eric Lippert
@bostIT,你在评论中提供的P vs. NP问题链接已经失效了。 - RBT
更新的链接:P vs. NP 问题 - devinbost
14个回答

160
你可能想阅读Eric Lippert的文章 "“Out Of Memory” Does Not Refer to Physical Memory"。简而言之,“内存不足”并不意味着可用内存量太小。最常见的原因是在当前地址空间中,没有足够大的连续内存块来满足所需的分配。如果你有100个块,每个块4 MB大,那么当你需要一个5 MB的块时,这些块是无用的。
关键点:
- 我认为,我们称之为“进程内存”的数据存储最好被视为磁盘上的一个大文件。 - RAM可以被看作是一种性能优化。 - 程序消耗的虚拟内存总量对其性能并不是非常重要。 - “内存不足”很少导致“内存不足”错误。相反,它会导致性能下降,因为实际上存储在磁盘上的存储成本突然变得重要。

如果你有100个块,每个块都是4MB大小,当你需要一个5MB的块时,这并没有什么帮助。我认为更好的措辞应该是进行微小的更正:“如果你有100个“空洞”块”。 - OfirD

47

请检查您正在构建的进程是否为64位,而不是32位。在Visual Studio的默认编译模式下,它是32位的。要做到这一点,请右键单击你的项目,选择属性 -> 构建 -> 平台目标:x64。与任何32位进程一样,以32位编译的Visual Studio应用程序具有2GB的虚拟内存限制。

64位进程没有此限制,因为它们使用64位指针,所以它们的理论最大地址空间(虚拟内存大小)为16 exabytes(2^64)。实际上,Windows x64将进程的虚拟内存限制为8TB。解决内存限制问题的方法是进行64位编译。

然而,在.NET中,对象的大小仍然默认限制为2GB。您可以创建多个数组,其组合大小将超过2GB,但默认情况下您无法创建大于2GB的数组。如果您仍想创建大于2GB的数组,则可以通过向app.config文件添加以下代码来实现:

<configuration>
  <runtime>
    <gcAllowVeryLargeObjects enabled="true" />
  </runtime>
</configuration>

对于 .NET Core:https://stackoverflow.com/a/70094295/970420 - undefined

26

由于您的内存碎片化,分配762MB所需的内存连续块不存在,分配器无法找到足够大的空隙来分配所需内存。

  1. 您可以尝试使用 /3GB(如其他人建议的那样)。
  2. 或切换到64位操作系统。
  3. 或修改算法,使其不需要一个大块的内存。也许可以分配几个较小(相对而言)的内存块。

10

你可能已经意识到了这个问题,那就是你试图分配一个大的连续内存块,但由于内存碎片化而无法实现。如果我需要做与你相同的事情,我会采取以下措施:

int sizeA = 10000,
    sizeB = 10000;
double sizeInMegabytes = (sizeA * sizeB * 8.0) / 1024.0 / 1024.0; //762 mb
double[][] randomNumbers = new double[sizeA][];
for (int i = 0; i < randomNumbers.Length; i++)
{
    randomNumbers[i] = new double[sizeB];
}

然后,要获取特定的索引,您将使用randomNumbers[i / sizeB][i % sizeB]

如果您总是按顺序访问值,另一种选择可能是使用重载的构造函数指定种子。这样,您将获得一个半随机数(例如DateTime.Now.Ticks),将其存储在变量中,然后每当您开始遍历列表时,都会使用原始种子创建一个新的Random实例:

private static int randSeed = (int)DateTime.Now.Ticks;  //Must stay the same unless you want to get different random numbers.
private static Random GetNewRandomIterator()
{
    return new Random(randSeed);
}
需要注意的是,虽然Fredrik Mörk回答中链接的博客表明问题通常是由于缺乏“地址空间”造成的,但它没有列出许多其他问题,比如2GB CLR对象大小限制(在同一博客的ShuggyCoUk的评论中提到),忽略了内存碎片化,并未提及页面文件大小的影响以及如何使用CreateFileMapping函数来解决此问题。
2GB限制意味着randomNumbers必须小于2GB。由于数组是类并具有一些开销,因此double数组需要比2 ^ 31小。我不确定Length需要比2 ^ 31小多少,但 .NET数组的开销? 表示为12-16字节。
内存碎片化非常类似于HDD碎片化。您可能拥有2GB的地址空间,但是当您创建和销毁对象时,值之间将存在间隙。如果这些间隙对于您的大型对象来说太小,并且无法请求额外空间,那么您将收到System.OutOfMemoryException。例如,如果您创建200万个1024字节对象,则使用1.9GB。如果您删除地址不是3的倍数的每个对象,则将使用0.6GB的内存,但是它会分散在地址空间中,并在之间保留2024字节的空块。如果您需要创建一个大小为0.2GB的对象,则无法这样做,因为没有足够大的块可以放置它并且不能获取额外空间(假设32位环境)。解决此问题的可能方法是使用较小的对象、减少存储在内存中的数据量或使用内存管理算法以限制/防止内存碎片化。应该注意的是,除非您正在开发使用大量内存的大型程序,否则这不会成为问题。此外,即使在64位系统上,此问题也可能出现,因为Windows主要受页面文件大小和系统上的RAM数量限制。
由于大多数程序从操作系统请求工作内存而不是请求文件映射,因此它们将受到系统RAM和页面文件大小的限制。正如Néstor Sánchez在博客上的评论中所指出的那样,在像C#这样的托管代码中,您受制于RAM /页面文件限制和操作系统的地址空间。

"由于大多数程序从操作系统请求工作内存而不是请求文件映射,您是否有任何进一步解释此问题的资源?当程序需要其工作集驻留在内存中(并受RAM大小限制)时,与程序可以写入磁盘并受连续内存限制(如所接受答案的博客所建议)时相比。" - waffles
1
@uMdRupert,我已经研究并编写了一段时间,所以我没有其他资源。你有机会阅读CreateFileMapping函数链接吗?你可能还想了解一下分页 - Trisped

5

我曾经遇到了和你相似的问题,将系统从32位升级到64位解决了我的问题。如果你使用的是64位电脑且不需要移植程序,那么这个方法也值得一试。


5
我建议不要使用/3GB Windows启动选项。除了其他问题(为了一个行为不良的应用程序而这样做有些过度,而且它可能无法解决你的问题),它可能会导致很多不稳定性。
许多Windows驱动程序没有经过此选项的测试,因此其中相当多的驱动程序假定用户模式指针总是指向地址空间的较低2GB。这意味着它们可能会在/3GB下出现严重错误。
然而,Windows通常将32位进程限制在2GB的地址空间中。但这并不意味着您应该期望能够分配2GB!
地址空间已经散布着各种已分配的数据。有堆栈、所有已加载的程序集、静态变量等等。无法保证任何地方都有800MB的连续未分配内存。
分配2个400MB块可能会更好。或者4个200MB块。在碎片化的内存空间中,较小的分配更容易找到空间。
无论如何,如果您打算将其部署到12GB机器上,您需要将其作为64位应用程序运行,这应该可以解决所有问题。

将工作分成较小的块似乎也没有帮助,请参见我上面的更新。 - m3ntat

3

不要分配一个巨大的数组,你可以尝试使用迭代器吗?它们是延迟执行的,也就是说只有在 foreach 语句中请求值时才生成值;这样你就不会因为内存不足而失败了:

private static IEnumerable<double> MakeRandomNumbers(int numberOfRandomNumbers) 
{
    for (int i = 0; i < numberOfRandomNumbers; i++)
    {
        yield return randomGenerator.GetAnotherRandomNumber();
    }
}


...

// Hooray, we won't run out of memory!
foreach(var number in MakeRandomNumbers(int.MaxValue))
{
    Console.WriteLine(number);
}

上述代码将生成您所需的任意数量的随机数,但仅在通过foreach语句请求时生成。这样就不会耗尽内存。
或者,如果您必须把它们都放在一个地方,请将它们存储在文件中而不是内存中。

有趣的方法,但我需要尽可能多地在其余应用程序的闲置时间中将其存储为随机数库,因为此应用程序在支持多个地理区域(多个蒙特卡罗模拟运行)的24小时时钟上运行,最高负载时间约为一天的70%。在一天的其余时间里,我想在所有空闲内存空间中缓冲随机数。将其存储到磁盘上太慢了,并且会破坏我通过缓冲到这个随机数内存缓存中所能获得的任何收益。 - m3ntat

3
如果您需要这样大的结构体,也许可以利用内存映射文件。 这篇文章可能会有所帮助。

1

32位Windows有2GB的进程内存限制。其他人提到的/3GB启动选项将使其变为3GB,只剩下1GB用于操作系统内核使用。实际上,如果您想要使用超过2GB而不会遇到麻烦,则需要64位操作系统。这也解决了一个问题,即尽管您可能拥有4GB的物理RAM,但视频卡所需的地址空间可能会使其中相当大一部分内存无法使用-通常约为500MB。


0
将您的解决方案转换为x64。如果仍然遇到问题,请授予所有抛出异常的内容最大长度,如下所示:
 var jsSerializer = new JavaScriptSerializer();
 jsSerializer.MaxJsonLength = Int32.MaxValue;

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