.NET OutOfMemoryException

时间:2010-11-22 18:27:26

标签: c# .net out-of-memory

为什么这样:

class OutOfMemoryTest02
{
    static void Main()
    {
        string value = new string('a', int.MaxValue);
    }
}

抛出异常;但这不会:

class OutOfMemoryTest
{
    private static void Main()
    {
        Int64 i = 0;
        ArrayList l = new ArrayList();
        while (true)
        {
            l.Add(new String('c', 1024));

            i++;
        }
    }
}

区别是什么?

9 个答案:

答案 0 :(得分:10)

您是否在文档中查找了int.MaxValue?它相当于2GB,这可能比你用于连续的“a”字符块的RAM更多 - 这就是你在这里所要求的。

http://msdn.microsoft.com/en-us/library/system.int32.maxvalue.aspx

你的无限循环最终会导致相同的异常(或者与RAM过度使用间接相关的异常),但这需要一段时间。尝试将1024增加到10 * 1024 * 1024以在循环情况下更快地重现症状。

当我使用这个更大的字符串大小运行时,我在68次循环后不到10秒内得到异常(检查i)。

答案 1 :(得分:7)

new string('a', int.MaxValue);

抛出OutOfMemoryException只是因为.NET的string有长度限制。 MSDN docs中的“备注”部分说:

  

内存中String对象的最大大小为2 GB,或大约10亿个字符。

在我的系统上(.NET 4.5 x64)new string('a', int.MaxValue/2 - 31)抛出,而new string('a', int.MaxValue/2 - 32)正常工作。

在第二个示例中,无限循环分配~2048个字节块,直到您的操作系统无法在虚拟地址空间中再分配任何块。达到此目的后,您也会获得OutOfMemoryException

(~2048字节= 1024个字符*每个UTF-16代码点2个字节+字符串开销字节)

试试这个great article的Eric。

答案 2 :(得分:5)

因为int.MaxValue是2,147,483,647,或者需要连续分配2千兆字节。

在第二个示例中,操作系统每次只需要找到1024个字节进行分配,并且可以交换到硬盘驱动器。我相信如果你让它运行得足够长,你最终会在一个黑暗的地方:)

答案 3 :(得分:4)

String对象可以使用支持共享字符串池来减少内存使用量。在前一种情况下,您生成一个几千兆字节的字符串。在第二种情况下,编译器可能会自动实例化该字符串,因此您生成一个1024字节的字符串,然后多次引用该字符串。

话虽这么说,那个大小的ArrayList应该让你没有内存,但是你可能没有让代码运行足够长的时间来耗尽内存。

答案 4 :(得分:3)

第二个片段也将崩溃。它只需要更长的时间,因为它消耗内存的速度要慢得多。注意你的硬盘访问指示灯,当Windows将页面从RAM中取出以腾出空间时,它会疯狂地闪烁。第一个字符串构造函数立即失败,因为堆管理器不允许您分配4千兆字节。

答案 5 :(得分:2)

两个版本都会导致OOM异常,只是(在32位计算机上)当你尝试分配一个“单个”非常大的对象时,你会立即得到第一个版本。

第二个版本将需要更长的时间,因为有很多因素会导致OOM条件出现大量的颠簸:

  • 您将分配数百万个可由GC访问的小对象。一旦你开始使系统处于压力之下,GC将花费大量的时间来扫描具有数百万和数百万个对象的世代。这将花费相当多的时间并开始对分页造成严重破坏,因为冷和热内存将是在扫描世代时不断地进出页面。

  • 当GC扫描数百万个对象以尝试释放内存时,会出现页面抖动。扫描将导致大量内存不断被分页。

颠簸将导致系统停止处理停止处理,因此OOM条件将需要很长时间才能达到。大部分时间都花费在GC和第二版的分页上。

答案 6 :(得分:1)

在您的第一个示例中,您尝试一次创建一个2g字符串

在第二个示例中,您继续向数组添加1k。您需要循环超过200万次才能达到相同的消耗量。

并且它也不是一次性存储在一个变量中。因此,我认为,你的一些内存使用可以持久保存到磁盘上,以便为新数据腾出空间。

答案 7 :(得分:1)

因为单个对象cannot have more than 2 GB

  

首先是一些背景;在2.0版本的.Net运行时(CLR)中,我们做出了有意识的设计决策,将GC Heap中允许的最大对象大小保持在2GB,即使是64位版本的运行时

在你的第一个例子中,你尝试分配一个2 GB的对象,对象开销(8字节?)它太大了。

我不知道ArrayList如何在内部工作,但是你分配了多个2 GB的对象和ArrayList - 据我所知 - 只保存4个指针(x64上的8个?)Bytes,无论对象有多大他们指的是。

引用another article

  

此外,引用其他对象的对象仅存储引用。因此,如果您有一个包含对其他三个对象的引用的对象,则内存占用量仅为12个额外字节:每个引用对象的一个​​32位指针。引用对象的大小无关紧要。

答案 8 :(得分:0)

你的系统停止运行的一个原因是因为.NET的代码更接近金属而且你处于一个紧凑的循环中,如果进程优先级允许它,它应该消耗100%的CPU。如果你想阻止应用程序在执行紧密循环时消耗太多CPU,你应该在循环结束时添加类似System.Threading.Thread.Sleep(10)的东西,这将强制产生到其他线程的处理时间。

JVM和.NET的CLR(公共语言运行时)之间的一个主要区别是CLR不限制x64系统/应用程序上的内存大小(在32位应用程序中,没有大地址识别标志操作系统限制)由于寻址限制,任何2GB的应用程序)。 JIT编译器为您的处理体系结构创建本机Windows代码,然后在与任何其他Windows应用程序运行相同的范围内运行它。 JVM是一个更加孤立的沙箱,它根据配置/命令行开关将应用程序限制为指定的大小。

至于两种算法之间的差异:

在具有足够连续内存的x64环境中运行时,单个字符串创建不能保证失败,以分配包含int.MaxValue字符所需的4GB(.NET字符串默认为Unicode,每个字符需要2个字节)。即使设置了大地址识别标志,32位应用程序也总是会失败,因为最大内存仍然是3.5GB。

你的代码的while循环版本可能会消耗更多的整体内存,只要你有足够的可用内容,然后抛出异常,因为你的字符串可以用较小的片段分配,但是最终确保它会出错(尽管如果你由于ArrayList超出了数组中元素的最大数量而不是无法为小字符串分配新空间,因此可能会有大量资源。 Kent Murra对于弦乐实习也是正确的;你需要随机化字符串的长度或字符内容以避免实习,否则你只是创建指向同一字符串的指针。 Steve Townsend建议增加字符串长度也会使找到足够大的连续内存块更难以实现,这将使异常更快地发生。

编辑:

以为我会给出一些人们可能会发现方便理解.NET内存的链接:

这两篇文章有点陈旧,但深度阅读非常好:

Garbage Collection: Automatic Memory Management in the Microsoft .NET Framework

Garbage Collection Part 2: Automatic Memory Management in the Microsoft .NET Framework

这些是来自.NET垃圾收集开发人员的博客,以获取有关.NET内存管理的更新版本的信息:

So, what’s new in the CLR 4.0 GC?

CLR 4.5: Maoni Stephens - Server Background GC

这个SO问题可以帮助您观察.NET内存的内部工作原理:

.NET Memory Profiling Tools