为什么我的C#应用程序会出现内存不足异常?

31

我的内存是4G物理内存,但为什么我创建了只有1.5G内存的对象时仍然会出现内存不足的异常?有什么想法吗?(同时我看到,在任务管理器的性能标签中内存并没有完全占用,并且我还可以在这里输入--所以内存实际上并不低,因此我认为我遇到了其他的内存限制)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestBigMemoryv1
{
    class MemoryHolderFoo
    {
        static Random seed = new Random();
        public Int32 holder1;
        public Int32 holder2;
        public Int64 holder3;

        public MemoryHolderFoo()
        {
            // prevent from optimized out
            holder1 = (Int32)seed.NextDouble();
            holder2 = (Int32)seed.NextDouble();
            holder3 = (Int64)seed.NextDouble();
        }
    }

    class Program
    {
        static int MemoryThreshold = 1500; //M
        static void Main(string[] args)
        {
            int persize = 16;
            int number = MemoryThreshold * 1000 * 1000/ persize;
            MemoryHolderFoo[] pool = new MemoryHolderFoo[number];
            for (int i = 0; i < number; i++)
            {
                pool[i] = new MemoryHolderFoo();
                if (i % 10000 == 0)
                {
                    Console.Write(".");
                }
            }

            return;
        }
    }
}

你为什么使用 Int32?而不是 int - Liam McInroy
7个回答

41
在一个普通的32位Windows应用程序中,进程只有2GB可寻址内存。这与可用的物理内存量无关。
因此,有2GB可用,但最多只能分配1.5GB。关键在于你的代码不是进程中唯一运行的代码。另外0.5GB可能是CLR加上进程中的碎片。
更新:在.NET 4.5中,在64位进程中启用gcAllowVeryLargeObjects设置后,可以拥有大型数组:
在64位平台上,启用大于2GB总大小的数组。数组中元素的最大数量为UInt32.MaxValue。
<configuration>
  <runtime>
    <gcAllowVeryLargeObjects enabled="true" />
  </runtime>
</configuration>

0.5GB也可以是机器上运行的除操作系统组件之外的所有其他内容。 - Robert C. Barth
5
不,每个进程都会获得一个完全可寻址的2GB虚拟内存空间。 - Nick
尼克是正确的。每个进程的地址空间都独立于其他进程。除非它们选择涉足共享内存。 - JaredPar
1
我自己做了更多的研究。32位系统存在限制的原因是应用程序使用虚拟地址访问内存,即使我们有超过4G的物理内存,但实际根本限制是虚拟内存地址空间,对吗? - George2
此外,如果您与执行大量小分配的本机代码进行交互,当您返回到.NET并且CLR尝试分配连续块(我相信LOH块大小在16MB和64MB之间,但我认为会有所不同)以扩展堆时,您可能也会遇到OutOfMemoryException。由于地址空间增加,这在64位上发生的可能性要小得多。 - Leon Breedt
显示剩余5条评论

10

除了其他点外,如果你想要访问大量的内存,请考虑使用 x64 - 但是请注意,最大单个对象大小仍然为2GB。并且由于在x64中引用较大,这意味着实际上您获得的是引用类型更小的最大数组/列表大小。当然,当您达到该限制时,您可能已经做错了很多事情!

其他选项:

  • 使用文件
  • 使用数据库

(显然与进程内存相比,这两者都有性能差异)


更新:在 .NET 4.5 之前的版本中,最大对象大小为2GB。从 4.5 开始,如果启用了 gcAllowVeryLargeObjects,则可以分配更大的对象。请注意,对于string的限制不受影响,但“数组”应该也包括“列表”,因为列表是由数组支持的。


1
当你说最大单个对象时,你是指CLR对象还是原始分配大小(本机或托管)。我假设前者,但想确认一下。此外,你有参考资料吗?我还没有看到过。不过,我无法想象为什么你要超过2GB的单个对象。 - JaredPar
请注意,最大单个对象大小仍为2GB。Marc,您是否有文件可以证明这个说法?我特别想知道单个对象的含义,因为我们可以将对象组合成新对象,所以在您的上下文中,单个对象是什么意思? - George2
1
Windows本身对win32进程强制实施2GB/3GB限制。32位引用的理论极限为4GB。Win64打破了这两个限制。 - Marc Gravell
谢谢Marc!我的理解正确吗?--“32位系统存在限制的原因是应用程序使用虚拟地址访问内存,即使我们有超过4G物理内存,但实际根本限制是虚拟内存地址空间。” - George2
请注意,在引用类型(类)的情况下,数组仅保存引用。在x86中,指针/引用为4个字节;在x64中,它为8个字节。因此,在最大数组中,您只能获得一半的引用:http://www.informit.com/guides/content.aspx?g=dotnet&seqNum=659 - Marc Gravell
显示剩余9条评论

5

请检查您是否正在构建64位进程,而不是32位进程,因为Visual Studio的默认编译模式是32位。要执行此操作,请右键单击项目,然后选择属性->生成->平台目标:x64。与任何32位进程一样,以32位编译的Visual Studio应用程序具有2GB的虚拟内存限制。

每个进程都有自己的虚拟内存,称为地址空间,它将其执行的代码和操作的数据映射到其中。 32位进程使用32位虚拟内存地址指针,这会创建绝对上限为4GB(2 ^ 32)的虚拟内存量的限制。 然而,操作系统需要其中的一半(引用自身的代码和数据),每个进程的限制为2GB。 如果您的32位应用程序尝试使用超过其地址空间的整个2GB,则会返回“System.OutOfMemory”,即使您计算机的物理内存没有满。

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

但是,默认情况下,Visual Studio中对象的大小仍然限制为2GB。 您将能够创建几个数组,它们的总大小将大于2GB,但您不能默认情况下创建大于2GB的数组。 希望,如果您仍然想创建大于2GB的数组,则可以通过将以下代码添加到应用程序配置文件中来执行:

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

+1 对于 Visual Studio 的构建属性 - 这使得我的应用程序可以使用它所需的略多于 4GB 的内存。 - ilasno

5

只是为了补充之前的回答:您可以在使用/3GB [以及可选的userva]引导标志启动的系统上超越2GB的限制。


1
虽然使用/3Gb开关,您将不得不修改可执行文件以手动设置其中的标志,以便能够利用引导标志。 - uzbones
我自己进行了更多的研究。32位系统存在限制的原因是应用程序使用虚拟地址访问内存,即使我们有超过4G物理内存,但实际上根本限制了虚拟内存地址空间,对吗? - George2

4

还有一件需要注意的事情; 一些.NET对象需要“连续”的内存。也就是说,如果您正在尝试分配一个大数组,则系统可能不仅需要您的进程中有足够的空闲内存,而且所有这些空闲内存都在一个大块中…不幸的是,进程内存随时间而碎片化,因此可能无法满足此要求。

有些对象/数据类型需要这个要求,有些则不需要...我记不清哪些需要了,但我似乎记得StringBuilder和MemoryStream有不同的要求。


4

正如其他人所提到的,作为32位应用程序,您最多可以使用2Gb可寻址内存。不要忘记开销。您正在创建一个包含9300万个对象的数组 - 如果每个对象有4个字节的开销,那么这将是额外的350Mb内存。


我自己进行了更多的研究。32位系统存在限制的原因是应用程序使用虚拟地址访问内存,即使我们有超过4G物理内存,但实际上根本限制了虚拟内存地址空间,对吗? - George2
1
是的,基本上就是这样。所有指针都存储在4个字节中,这限制了它们能够查看的范围。在16位指针时代,情况甚至更糟糕。别问我关于段:偏移或窗口高内存的事情... - geofftnz

3

嗨,uzbones,这是否意味着4G内存是无用的?我的应用程序最多只能消耗2G吗? - George2
好的...不,如果你有超过4G的内存,你可以运行两个程序副本,每个副本消耗2G的内存。正如KristoferA在下面进一步提到的那样,还有一个系统开关可以用来改变内存限制为3G,或者你需要使用64位系统。 - uzbones
我自己进行了更多的研究。32位系统存在限制的原因是因为应用程序使用虚拟地址来访问内存,即使我们有超过4G物理内存,但实际上根本限制了虚拟内存地址空间,对吗? - George2
是的,在32位系统中,为了访问超过4G的内存(2G用户模式和2G系统),操作系统需要使用比32位int更大的索引。您可以通过使用AppDomains来解决此问题http://en.csharp-online.net/.NET_Architecture%E2%80%94Application_domains。 - uzbones

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